Skip to content

Commit 92376ce

Browse files
Fix IAsyncEnumerable controller methods to allow setting headers (#57924)
* Fix IAsyncEnumerable controller methods to allow setting headers * name * httpjson extensions too * revert
1 parent 8371724 commit 92376ce

File tree

7 files changed

+117
-92
lines changed

7 files changed

+117
-92
lines changed

src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs

Lines changed: 22 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -90,22 +90,12 @@ public static Task WriteAsJsonAsync<TValue>(
9090

9191
response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;
9292

93-
var startTask = Task.CompletedTask;
94-
if (!response.HasStarted)
95-
{
96-
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
97-
startTask = response.StartAsync(cancellationToken);
98-
}
99-
10093
// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
101-
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
94+
if (!cancellationToken.CanBeCanceled)
10295
{
103-
return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, options,
104-
ignoreOCE: !cancellationToken.CanBeCanceled,
105-
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
96+
return WriteAsJsonAsyncSlow(response.BodyWriter, value, options, response.HttpContext.RequestAborted);
10697
}
10798

108-
startTask.GetAwaiter().GetResult();
10999
return JsonSerializer.SerializeAsync(response.BodyWriter, value, options, cancellationToken);
110100
}
111101

@@ -131,33 +121,22 @@ public static Task WriteAsJsonAsync<TValue>(
131121

132122
response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;
133123

134-
var startTask = Task.CompletedTask;
135-
if (!response.HasStarted)
136-
{
137-
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
138-
startTask = response.StartAsync(cancellationToken);
139-
}
140-
141124
// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
142-
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
125+
if (!cancellationToken.CanBeCanceled)
143126
{
144-
return WriteAsJsonAsyncSlow(startTask, response, value, jsonTypeInfo,
145-
ignoreOCE: !cancellationToken.CanBeCanceled,
146-
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
127+
return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo, response.HttpContext.RequestAborted);
147128
}
148129

149-
startTask.GetAwaiter().GetResult();
150130
return JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);
151131

152-
static async Task WriteAsJsonAsyncSlow(Task startTask, HttpResponse response, TValue value, JsonTypeInfo<TValue> jsonTypeInfo,
153-
bool ignoreOCE, CancellationToken cancellationToken)
132+
static async Task WriteAsJsonAsyncSlow(HttpResponse response, TValue value, JsonTypeInfo<TValue> jsonTypeInfo,
133+
CancellationToken cancellationToken)
154134
{
155135
try
156136
{
157-
await startTask;
158137
await JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);
159138
}
160-
catch (OperationCanceledException) when (ignoreOCE) { }
139+
catch (OperationCanceledException) { }
161140
}
162141
}
163142

@@ -184,52 +163,38 @@ public static Task WriteAsJsonAsync(
184163

185164
response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;
186165

187-
var startTask = Task.CompletedTask;
188-
if (!response.HasStarted)
189-
{
190-
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
191-
startTask = response.StartAsync(cancellationToken);
192-
}
193-
194166
// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
195-
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
167+
if (!cancellationToken.CanBeCanceled)
196168
{
197-
return WriteAsJsonAsyncSlow(startTask, response, value, jsonTypeInfo,
198-
ignoreOCE: !cancellationToken.CanBeCanceled,
199-
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
169+
return WriteAsJsonAsyncSlow(response, value, jsonTypeInfo, response.HttpContext.RequestAborted);
200170
}
201171

202-
startTask.GetAwaiter().GetResult();
203172
return JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);
204173

205-
static async Task WriteAsJsonAsyncSlow(Task startTask, HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo,
206-
bool ignoreOCE, CancellationToken cancellationToken)
174+
static async Task WriteAsJsonAsyncSlow(HttpResponse response, object? value, JsonTypeInfo jsonTypeInfo,
175+
CancellationToken cancellationToken)
207176
{
208177
try
209178
{
210-
await startTask;
211179
await JsonSerializer.SerializeAsync(response.BodyWriter, value, jsonTypeInfo, cancellationToken);
212180
}
213-
catch (OperationCanceledException) when (ignoreOCE) { }
181+
catch (OperationCanceledException) { }
214182
}
215183
}
216184

217185
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
218186
[RequiresDynamicCode(RequiresDynamicCodeMessage)]
219187
private static async Task WriteAsJsonAsyncSlow<TValue>(
220-
Task startTask,
221188
PipeWriter body,
222189
TValue value,
223190
JsonSerializerOptions? options,
224-
bool ignoreOCE,
225191
CancellationToken cancellationToken)
226192
{
227193
try
228194
{
229-
await startTask;
230195
await JsonSerializer.SerializeAsync(body, value, options, cancellationToken);
231196
}
232-
catch (OperationCanceledException) when (ignoreOCE) { }
197+
catch (OperationCanceledException) { }
233198
}
234199

235200
/// <summary>
@@ -304,42 +269,30 @@ public static Task WriteAsJsonAsync(
304269

305270
response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;
306271

307-
var startTask = Task.CompletedTask;
308-
if (!response.HasStarted)
309-
{
310-
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
311-
startTask = response.StartAsync(cancellationToken);
312-
}
313-
314272
// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
315-
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
273+
if (!cancellationToken.CanBeCanceled)
316274
{
317-
return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, type, options,
318-
ignoreOCE: !cancellationToken.CanBeCanceled,
319-
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
275+
return WriteAsJsonAsyncSlow(response.BodyWriter, value, type, options,
276+
response.HttpContext.RequestAborted);
320277
}
321278

322-
startTask.GetAwaiter().GetResult();
323279
return JsonSerializer.SerializeAsync(response.BodyWriter, value, type, options, cancellationToken);
324280
}
325281

326282
[RequiresUnreferencedCode(RequiresUnreferencedCodeMessage)]
327283
[RequiresDynamicCode(RequiresDynamicCodeMessage)]
328284
private static async Task WriteAsJsonAsyncSlow(
329-
Task startTask,
330285
PipeWriter body,
331286
object? value,
332287
Type type,
333288
JsonSerializerOptions? options,
334-
bool ignoreOCE,
335289
CancellationToken cancellationToken)
336290
{
337291
try
338292
{
339-
await startTask;
340293
await JsonSerializer.SerializeAsync(body, value, type, options, cancellationToken);
341294
}
342-
catch (OperationCanceledException) when (ignoreOCE) { }
295+
catch (OperationCanceledException) { }
343296
}
344297

345298
/// <summary>
@@ -367,33 +320,22 @@ public static Task WriteAsJsonAsync(
367320

368321
response.ContentType = contentType ?? ContentTypeConstants.JsonContentTypeWithCharset;
369322

370-
var startTask = Task.CompletedTask;
371-
if (!response.HasStarted)
372-
{
373-
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
374-
startTask = response.StartAsync(cancellationToken);
375-
}
376-
377323
// if no user provided token, pass the RequestAborted token and ignore OperationCanceledException
378-
if (!startTask.IsCompleted || !cancellationToken.CanBeCanceled)
324+
if (!cancellationToken.CanBeCanceled)
379325
{
380-
return WriteAsJsonAsyncSlow(startTask, response.BodyWriter, value, type, context,
381-
ignoreOCE: !cancellationToken.CanBeCanceled,
382-
cancellationToken.CanBeCanceled ? cancellationToken : response.HttpContext.RequestAborted);
326+
return WriteAsJsonAsyncSlow(response.BodyWriter, value, type, context, response.HttpContext.RequestAborted);
383327
}
384328

385-
startTask.GetAwaiter().GetResult();
386329
return JsonSerializer.SerializeAsync(response.BodyWriter, value, type, context, cancellationToken);
387330

388-
static async Task WriteAsJsonAsyncSlow(Task startTask, PipeWriter body, object? value, Type type, JsonSerializerContext context,
389-
bool ignoreOCE, CancellationToken cancellationToken)
331+
static async Task WriteAsJsonAsyncSlow(PipeWriter body, object? value, Type type, JsonSerializerContext context,
332+
CancellationToken cancellationToken)
390333
{
391334
try
392335
{
393-
await startTask;
394336
await JsonSerializer.SerializeAsync(body, value, type, context, cancellationToken);
395337
}
396-
catch (OperationCanceledException) when (ignoreOCE) { }
338+
catch (OperationCanceledException) { }
397339
}
398340
}
399341

src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.IO.Pipelines;
45
using System.Runtime.CompilerServices;
56
using System.Text;
67
using System.Text.Json;
78
using System.Text.Json.Serialization;
89
using System.Text.Json.Serialization.Metadata;
9-
using Microsoft.AspNetCore.InternalTesting;
10+
using Microsoft.AspNetCore.Builder;
11+
using Microsoft.AspNetCore.Http.Features;
12+
using Microsoft.AspNetCore.TestHost;
1013

1114
#nullable enable
1215

@@ -481,6 +484,71 @@ public async Task WriteAsJsonAsync_NullValue_WithJsonTypeInfo_JsonResponse()
481484
Assert.Equal("null", data);
482485
}
483486

487+
// Regression test: https://github.com/dotnet/aspnetcore/issues/57895
488+
[Fact]
489+
public async Task AsyncEnumerableCanSetHeader()
490+
{
491+
var builder = WebApplication.CreateBuilder();
492+
builder.WebHost.UseTestServer();
493+
494+
await using var app = builder.Build();
495+
496+
app.MapGet("/", IAsyncEnumerable<int> (HttpContext httpContext) =>
497+
{
498+
return AsyncEnum();
499+
500+
async IAsyncEnumerable<int> AsyncEnum()
501+
{
502+
await Task.Yield();
503+
httpContext.Response.Headers["Test"] = "t";
504+
yield return 1;
505+
}
506+
});
507+
508+
await app.StartAsync();
509+
510+
var client = app.GetTestClient();
511+
512+
var result = await client.GetAsync("/");
513+
result.EnsureSuccessStatusCode();
514+
var headerValue = Assert.Single(result.Headers.GetValues("Test"));
515+
Assert.Equal("t", headerValue);
516+
517+
await app.StopAsync();
518+
}
519+
520+
// Regression test: https://github.com/dotnet/aspnetcore/issues/57895
521+
[Fact]
522+
public async Task EnumerableCanSetHeader()
523+
{
524+
var builder = WebApplication.CreateBuilder();
525+
builder.WebHost.UseTestServer();
526+
527+
await using var app = builder.Build();
528+
529+
app.MapGet("/", IEnumerable<int> (HttpContext httpContext) =>
530+
{
531+
return Enum();
532+
533+
IEnumerable<int> Enum()
534+
{
535+
httpContext.Response.Headers["Test"] = "t";
536+
yield return 1;
537+
}
538+
});
539+
540+
await app.StartAsync();
541+
542+
var client = app.GetTestClient();
543+
544+
var result = await client.GetAsync("/");
545+
result.EnsureSuccessStatusCode();
546+
var headerValue = Assert.Single(result.Headers.GetValues("Test"));
547+
Assert.Equal("t", headerValue);
548+
549+
await app.StopAsync();
550+
}
551+
484552
public class TestObject
485553
{
486554
public string? StringProperty { get; set; }

src/Http/Http.Extensions/test/Microsoft.AspNetCore.Http.Extensions.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Reference Include="Microsoft.AspNetCore.Http.Results" />
1919
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
2020
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
21+
<Reference Include="Microsoft.AspNetCore.TestHost" />
2122
<Reference Include="Microsoft.Extensions.DependencyInjection" />
2223
<Reference Include="Microsoft.Extensions.DependencyModel" />
2324
</ItemGroup>

src/Mvc/Mvc.Core/src/Formatters/SystemTextJsonOutputFormatter.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,6 @@ public sealed override async Task WriteResponseBodyAsync(OutputFormatterWriteCon
8888
try
8989
{
9090
var responseWriter = httpContext.Response.BodyWriter;
91-
if (!httpContext.Response.HasStarted)
92-
{
93-
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
94-
await httpContext.Response.StartAsync();
95-
}
9691

9792
if (jsonTypeInfo is not null)
9893
{

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

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,6 @@ public async Task ExecuteAsync(ActionContext context, JsonResult result)
6666
try
6767
{
6868
var responseWriter = response.BodyWriter;
69-
if (!response.HasStarted)
70-
{
71-
// Flush headers before starting Json serialization. This avoids an extra layer of buffering before the first flush.
72-
await response.StartAsync();
73-
}
74-
7569
await JsonSerializer.SerializeAsync(responseWriter, value, objectType, jsonSerializerOptions, context.HttpContext.RequestAborted);
7670
}
7771
catch (OperationCanceledException) when (context.HttpContext.RequestAborted.IsCancellationRequested) { }

src/Mvc/test/Mvc.FunctionalTests/SystemTextJsonOutputFormatterTest.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,21 @@ public async Task Formatting_PolymorphicModel_WithJsonPolymorphism()
6565
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
6666
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
6767
}
68+
69+
// Regression test: https://github.com/dotnet/aspnetcore/issues/57895
70+
[Fact]
71+
public async Task CanSetHeaderWithAsyncEnumerable()
72+
{
73+
// Arrange
74+
var expected = "[1]";
75+
76+
// Act
77+
var response = await Client.GetAsync($"/SystemTextJsonOutputFormatter/{nameof(SystemTextJsonOutputFormatterController.AsyncEnumerable)}");
78+
79+
// Assert
80+
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
81+
Assert.Equal(expected, await response.Content.ReadAsStringAsync());
82+
var headerValue = Assert.Single(response.Headers.GetValues("Test"));
83+
Assert.Equal("t", headerValue);
84+
}
6885
}

src/Mvc/test/WebSites/FormatterWebSite/Controllers/SystemTextJsonOutputFormatterController.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ public class SystemTextJsonOutputFormatterController : ControllerBase
1919
Address = "Some address",
2020
};
2121

22+
[HttpGet]
23+
public async IAsyncEnumerable<int> AsyncEnumerable()
24+
{
25+
await Task.Yield();
26+
HttpContext.Response.Headers["Test"] = "t";
27+
yield return 1;
28+
}
29+
2230
[JsonPolymorphic]
2331
[JsonDerivedType(typeof(DerivedModel), nameof(DerivedModel))]
2432
public class SimpleModel

0 commit comments

Comments
 (0)