Skip to content

Commit efe0bf0

Browse files
committed
Add MVC filter to remove result if End() was called
1 parent 9fc7391 commit efe0bf0

File tree

8 files changed

+130
-53
lines changed

8 files changed

+130
-53
lines changed

src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/BufferResponseStreamMiddleware.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,20 @@ public BufferResponseStreamMiddleware(RequestDelegate next, ILogger<BufferRespon
2323
}
2424

2525
public Task InvokeAsync(HttpContextCore context)
26-
=> context.GetEndpoint()?.Metadata.GetMetadata<BufferResponseStreamAttribute>() is { IsDisabled: false } metadata && context.Features.Get<IHttpResponseBodyFeature>() is { } feature
27-
? BufferResponseStreamAsync(context, feature, metadata)
26+
=> context.GetEndpoint()?.Metadata.GetMetadata<BufferResponseStreamAttribute>() is { IsDisabled: false } metadata
27+
? BufferResponseStreamAsync(context, metadata)
2828
: _next(context);
2929

30-
private async Task BufferResponseStreamAsync(HttpContextCore context, IHttpResponseBodyFeature feature, BufferResponseStreamAttribute metadata)
30+
private async Task BufferResponseStreamAsync(HttpContextCore context, BufferResponseStreamAttribute metadata)
3131
{
3232
LogBuffering(metadata.BufferLimit, metadata.MemoryThreshold);
3333

34-
var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
35-
var originalBufferedResponseFeature = context.Features.Get<IBufferedResponseFeature>();
34+
var responseBodyFeature = context.Features.GetRequired<IHttpResponseBodyFeature>();
3635

37-
await using var bufferedFeature = new BufferedHttpResponseFeature(feature, metadata);
36+
await using var bufferedFeature = new HttpRequestAdapterFeature(responseBodyFeature, metadata);
3837

3938
context.Features.Set<IHttpResponseBodyFeature>(bufferedFeature);
40-
context.Features.Set<IBufferedResponseFeature>(bufferedFeature);
39+
context.Features.Set<IHttpRequestAdapterFeature>(bufferedFeature);
4140

4241
try
4342
{
@@ -46,8 +45,8 @@ private async Task BufferResponseStreamAsync(HttpContextCore context, IHttpRespo
4645
}
4746
finally
4847
{
49-
context.Features.Set(originalBodyFeature);
50-
context.Features.Set(originalBufferedResponseFeature);
48+
context.Features.Set(responseBodyFeature);
49+
context.Features.Set<IHttpRequestAdapterFeature>(null);
5150
}
5251
}
5352
}

src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/BufferedHttpResponseFeature.cs renamed to src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/HttpRequestAdapterFeature.cs

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,20 @@
1212

1313
namespace Microsoft.AspNetCore.SystemWebAdapters;
1414

15-
internal class BufferedHttpResponseFeature : Stream, IHttpResponseBodyFeature, IBufferedResponseFeature
15+
/// <summary>
16+
/// This feature implements the <see cref="IHttpRequestAdapterFeature"/> to expose functionality for the adapters. As part of that,
17+
/// it overrides the implementations of two other features:
18+
///
19+
/// <list>
20+
/// <item>
21+
/// <see cref="IHttpResponseFeature"/>: On ASP.NET Framework, after End() was called, it would not throw if headers/status/etc was changed.
22+
/// </item>
23+
/// <item>
24+
/// <see cref="IHttpResponseBodyFeature"/>: Provide ability to turn off writing to the stream, while also supporting the ability to clear and suppress output
25+
/// </item>
26+
/// </list>
27+
/// </summary>
28+
internal class HttpRequestAdapterFeature : Stream, IHttpResponseBodyFeature, IHttpRequestAdapterFeature
1629
{
1730
public enum StreamState
1831
{
@@ -22,16 +35,17 @@ public enum StreamState
2235
Complete,
2336
}
2437

25-
private readonly IHttpResponseBodyFeature _other;
38+
private readonly IHttpResponseBodyFeature _responseBodyFeature;
2639
private readonly BufferResponseStreamAttribute _metadata;
2740

2841
private FileBufferingWriteStream? _bufferedStream;
2942
private PipeWriter? _pipeWriter;
3043

31-
public BufferedHttpResponseFeature(IHttpResponseBodyFeature other, BufferResponseStreamAttribute metadata)
44+
public HttpRequestAdapterFeature(IHttpResponseBodyFeature httpResponseBody, BufferResponseStreamAttribute metadata)
3245
{
33-
_other = other;
46+
_responseBodyFeature = httpResponseBody;
3447
_metadata = metadata;
48+
3549
State = StreamState.NotStarted;
3650
}
3751

@@ -49,11 +63,11 @@ private Stream CurrentStream
4963
{
5064
if (State == StreamState.NotBuffering)
5165
{
52-
return _other.Stream;
66+
return _responseBodyFeature.Stream;
5367
}
5468
else if (State == StreamState.Complete)
5569
{
56-
return Stream.Null;
70+
return Null;
5771
}
5872
else
5973
{
@@ -63,7 +77,7 @@ private Stream CurrentStream
6377
}
6478
}
6579

66-
public void End() => CompleteAsync().GetAwaiter().GetResult();
80+
public Task EndAsync() => CompleteAsync();
6781

6882
public override async ValueTask DisposeAsync()
6983
{
@@ -79,7 +93,7 @@ public async ValueTask FlushBufferedStreamAsync()
7993
{
8094
if (State is StreamState.Buffering && _bufferedStream is not null && !SuppressContent)
8195
{
82-
await _bufferedStream.DrainBufferAsync(_other.Stream);
96+
await _bufferedStream.DrainBufferAsync(_responseBodyFeature.Stream);
8397
}
8498
}
8599

@@ -97,10 +111,21 @@ public override long Position
97111
set => throw new NotSupportedException();
98112
}
99113

114+
public bool IsEnded => State == StreamState.Complete;
115+
116+
public void ClearContent()
117+
{
118+
if (_bufferedStream is not null)
119+
{
120+
_bufferedStream.Dispose();
121+
_bufferedStream = null;
122+
}
123+
}
124+
100125
public async Task CompleteAsync()
101126
{
102127
await FlushBufferedStreamAsync();
103-
await _other.CompleteAsync();
128+
await _responseBodyFeature.CompleteAsync();
104129
State = StreamState.Complete;
105130
}
106131

@@ -109,8 +134,8 @@ public void DisableBuffering()
109134
if (State == StreamState.NotStarted)
110135
{
111136
State = StreamState.NotBuffering;
112-
_other.DisableBuffering();
113-
_pipeWriter = _other.Writer;
137+
_responseBodyFeature.DisableBuffering();
138+
_pipeWriter = _responseBodyFeature.Writer;
114139
}
115140
}
116141

@@ -134,7 +159,7 @@ public Task StartAsync(CancellationToken cancellationToken = default)
134159
State = StreamState.Buffering;
135160
}
136161

137-
return _other.StartAsync(cancellationToken);
162+
return _responseBodyFeature.StartAsync(cancellationToken);
138163
}
139164

140165
public override void Write(byte[] buffer, int offset, int count) => CurrentStream.Write(buffer, offset, count);
@@ -148,13 +173,4 @@ public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationTo
148173

149174
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
150175
=> CurrentStream.WriteAsync(buffer, offset, count, cancellationToken);
151-
152-
public void ClearContent()
153-
{
154-
if (_bufferedStream is not null)
155-
{
156-
_bufferedStream.Dispose();
157-
_bufferedStream = null;
158-
}
159-
}
160176
}
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
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;
4+
using System.Threading.Tasks;
55

66
namespace Microsoft.AspNetCore.SystemWebAdapters;
77

8-
internal interface IBufferedResponseFeature
8+
internal interface IHttpRequestAdapterFeature
99
{
10-
Stream Stream { get; }
10+
bool IsEnded { get; }
1111

1212
bool SuppressContent { get; set; }
1313

14-
void End();
14+
Task EndAsync();
1515

1616
void ClearContent();
1717
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.SystemWebAdapters;
6+
using Microsoft.AspNetCore.SystemWebAdapters.Mvc;
7+
8+
namespace Microsoft.Extensions.DependencyInjection;
9+
10+
internal static class MvcExtensions
11+
{
12+
public static ISystemWebAdapterBuilder AddMvc(this ISystemWebAdapterBuilder builder)
13+
{
14+
builder.Services.AddTransient<ResponseEndFilter>();
15+
16+
builder.Services.AddOptions<MvcOptions>()
17+
.Configure(options =>
18+
{
19+
// We want the check for HttpResponse.End() to be done as soon as possible after the action is run.
20+
// This will minimize any chance that output will be written which will fail since the response has completed.
21+
options.Filters.Add<ResponseEndFilter>(int.MaxValue);
22+
});
23+
24+
return builder;
25+
}
26+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Mvc.Filters;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace Microsoft.AspNetCore.SystemWebAdapters.Mvc;
8+
9+
internal partial class ResponseEndFilter : IActionFilter
10+
{
11+
private readonly ILogger<ResponseEndFilter> _logger;
12+
13+
[LoggerMessage(0, LogLevel.Trace, "Clearing MVC result since HttpResponse.End() was called")]
14+
private partial void LogClearingResult();
15+
16+
public ResponseEndFilter(ILogger<ResponseEndFilter> logger)
17+
{
18+
_logger = logger;
19+
}
20+
21+
public void OnActionExecuted(ActionExecutedContext context)
22+
{
23+
if (context.Result is not null && context.HttpContext.Features.Get<IHttpRequestAdapterFeature>() is { IsEnded: true })
24+
{
25+
LogClearingResult();
26+
context.Result = null;
27+
}
28+
}
29+
30+
public void OnActionExecuting(ActionExecutingContext context)
31+
{
32+
}
33+
}

src/Microsoft.AspNetCore.SystemWebAdapters/Adapters/SystemWebAdaptersExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
using System.Web.Configuration;
77
using Microsoft.AspNetCore.Builder;
88
using Microsoft.AspNetCore.Hosting;
9+
using Microsoft.AspNetCore.Mvc;
910
using Microsoft.AspNetCore.SystemWebAdapters;
11+
using Microsoft.AspNetCore.SystemWebAdapters.Mvc;
1012

1113
namespace Microsoft.Extensions.DependencyInjection;
1214

@@ -19,7 +21,8 @@ public static ISystemWebAdapterBuilder AddSystemWebAdapters(this IServiceCollect
1921
services.AddSingleton<BrowserCapabilitiesFactory>();
2022
services.AddTransient<IStartupFilter, HttpContextStartupFilter>();
2123

22-
return new SystemWebAdapterBuilder(services);
24+
return new SystemWebAdapterBuilder(services)
25+
.AddMvc();
2326
}
2427

2528
public static void UseSystemWebAdapters(this IApplicationBuilder app)

src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class HttpResponse
2525

2626
private NameValueCollection? _headers;
2727
private ResponseHeaders? _typedHeaders;
28-
private IBufferedResponseFeature? _bufferedFeature;
28+
private IHttpRequestAdapterFeature? _adapterFeature;
2929
private TextWriter? _writer;
3030
private HttpCookieCollection? _cookies;
3131

@@ -34,8 +34,8 @@ internal HttpResponse(HttpResponseCore response)
3434
_response = response;
3535
}
3636

37-
private IBufferedResponseFeature BufferedFeature => _bufferedFeature ??= _response.HttpContext.Features.Get<IBufferedResponseFeature>()
38-
?? throw new InvalidOperationException("Response buffering must be enabled on this endpoint for this feature via the IBufferResponseStreamMetadata metadata item");
37+
private IHttpRequestAdapterFeature AdapterFeature => _adapterFeature ??= _response.HttpContext.Features.Get<IHttpRequestAdapterFeature>()
38+
?? throw new InvalidOperationException($"Response buffering must be enabled on this endpoint for this API via the {nameof(BufferResponseStreamAttribute)} metadata item");
3939

4040
internal ResponseHeaders TypedHeaders => _typedHeaders ??= new(_response.Headers);
4141

@@ -82,8 +82,8 @@ public bool TrySkipIisCustomErrors
8282

8383
public bool SuppressContent
8484
{
85-
get => BufferedFeature.SuppressContent;
86-
set => BufferedFeature.SuppressContent = value;
85+
get => AdapterFeature.SuppressContent;
86+
set => AdapterFeature.SuppressContent = value;
8787
}
8888

8989
public Encoding ContentEncoding
@@ -201,7 +201,7 @@ private void Redirect(string url, bool endResponse, bool permanent)
201201

202202
public void SetCookie(HttpCookie cookie) => Cookies.Set(cookie);
203203

204-
public void End() => BufferedFeature.End();
204+
public void End() => AdapterFeature.EndAsync().GetAwaiter().GetResult();
205205

206206
public void Write(char ch) => Output.Write(ch);
207207

@@ -233,7 +233,7 @@ public void ClearContent()
233233
}
234234
else
235235
{
236-
BufferedFeature.ClearContent();
236+
AdapterFeature.ClearContent();
237237
}
238238
}
239239

0 commit comments

Comments
 (0)