Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,20 @@ public BufferResponseStreamMiddleware(RequestDelegate next, ILogger<BufferRespon
}

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

private async Task BufferResponseStreamAsync(HttpContextCore context, IHttpResponseBodyFeature feature, BufferResponseStreamAttribute metadata)
private async Task BufferResponseStreamAsync(HttpContextCore context, BufferResponseStreamAttribute metadata)
{
LogBuffering(metadata.BufferLimit, metadata.MemoryThreshold);

var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
var originalBufferedResponseFeature = context.Features.Get<IBufferedResponseFeature>();
var responseBodyFeature = context.Features.GetRequired<IHttpResponseBodyFeature>();

await using var bufferedFeature = new BufferedHttpResponseFeature(feature, metadata);
await using var bufferedFeature = new HttpRequestAdapterFeature(responseBodyFeature, metadata);

context.Features.Set<IHttpResponseBodyFeature>(bufferedFeature);
context.Features.Set<IBufferedResponseFeature>(bufferedFeature);
context.Features.Set<IHttpRequestAdapterFeature>(bufferedFeature);

try
{
Expand All @@ -46,8 +45,8 @@ private async Task BufferResponseStreamAsync(HttpContextCore context, IHttpRespo
}
finally
{
context.Features.Set(originalBodyFeature);
context.Features.Set(originalBufferedResponseFeature);
context.Features.Set(responseBodyFeature);
context.Features.Set<IHttpRequestAdapterFeature>(null);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,59 +12,106 @@

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal class BufferedHttpResponseFeature : Stream, IHttpResponseBodyFeature, IBufferedResponseFeature
/// <summary>
/// This feature implements the <see cref="IHttpRequestAdapterFeature"/> to expose functionality for the adapters. As part of that,
/// it overrides the following features as well:
///
/// <list>
/// <item>
/// <see cref="IHttpResponseBodyFeature"/>: Provide ability to turn off writing to the stream, while also supporting the ability to clear and suppress output
/// </item>
/// </list>
/// </summary>
internal class HttpRequestAdapterFeature : Stream, IHttpResponseBodyFeature, IHttpRequestAdapterFeature
{
public enum StreamState
private enum StreamState
{
NotStarted,
Buffering,
NotBuffering,
Complete,
}

private readonly IHttpResponseBodyFeature _other;
private readonly IHttpResponseBodyFeature _responseBodyFeature;
private readonly BufferResponseStreamAttribute _metadata;

private FileBufferingWriteStream? _bufferedStream;
private PipeWriter? _pipeWriter;
private bool _suppressContent;
private StreamState _state;

public BufferedHttpResponseFeature(IHttpResponseBodyFeature other, BufferResponseStreamAttribute metadata)
public HttpRequestAdapterFeature(IHttpResponseBodyFeature httpResponseBody, BufferResponseStreamAttribute metadata)
{
_other = other;
_responseBodyFeature = httpResponseBody;
_metadata = metadata;
State = StreamState.NotStarted;
_state = StreamState.NotStarted;
}

public StreamState State { get; private set; }
Task IHttpResponseBodyFeature.CompleteAsync() => CompleteAsync();

public Stream Stream => this;
void IHttpResponseBodyFeature.DisableBuffering()
{
if (_state == StreamState.NotStarted)
{
_state = StreamState.NotBuffering;
_responseBodyFeature.DisableBuffering();
_pipeWriter = _responseBodyFeature.Writer;
}
}

Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken)
{
if (_state == StreamState.NotStarted)
{
_state = StreamState.Buffering;
}

return _responseBodyFeature.StartAsync(cancellationToken);
}

Stream IHttpResponseBodyFeature.Stream => this;

public PipeWriter Writer => _pipeWriter ??= PipeWriter.Create(this, new StreamPipeWriterOptions(leaveOpen: true));
PipeWriter IHttpResponseBodyFeature.Writer => _pipeWriter ??= PipeWriter.Create(this, new StreamPipeWriterOptions(leaveOpen: true));

public bool SuppressContent { get; set; }
bool IHttpRequestAdapterFeature.SuppressContent
{
get => _suppressContent;
set => _suppressContent = value;
}

Task IHttpRequestAdapterFeature.EndAsync() => CompleteAsync();

bool IHttpRequestAdapterFeature.IsEnded => _state == StreamState.Complete;

void IHttpRequestAdapterFeature.ClearContent()
{
if (_bufferedStream is not null)
{
_bufferedStream.Dispose();
_bufferedStream = null;
}
}

private Stream CurrentStream
{
get
{
if (State == StreamState.NotBuffering)
if (_state == StreamState.NotBuffering)
{
return _other.Stream;
return _responseBodyFeature.Stream;
}
else if (State == StreamState.Complete)
else if (_state == StreamState.Complete)
{
return Stream.Null;
return Null;
}
else
{
State = StreamState.Buffering;
_state = StreamState.Buffering;
return _bufferedStream ??= new FileBufferingWriteStream(_metadata.MemoryThreshold, _metadata.BufferLimit);
}
}
}

public void End() => CompleteAsync().GetAwaiter().GetResult();

public override async ValueTask DisposeAsync()
{
if (_bufferedStream is not null)
Expand All @@ -77,9 +124,9 @@ public override async ValueTask DisposeAsync()

public async ValueTask FlushBufferedStreamAsync()
{
if (State is StreamState.Buffering && _bufferedStream is not null && !SuppressContent)
if (_state is StreamState.Buffering && _bufferedStream is not null && !_suppressContent)
{
await _bufferedStream.DrainBufferAsync(_other.Stream);
await _bufferedStream.DrainBufferAsync(_responseBodyFeature.Stream);
}
}

Expand All @@ -97,21 +144,12 @@ public override long Position
set => throw new NotSupportedException();
}

public async Task CompleteAsync()
{
await FlushBufferedStreamAsync();
await _other.CompleteAsync();
State = StreamState.Complete;
}

public void DisableBuffering()
private async Task CompleteAsync()
{
if (State == StreamState.NotStarted)
{
State = StreamState.NotBuffering;
_other.DisableBuffering();
_pipeWriter = _other.Writer;
}
await FlushBufferedStreamAsync();
await _responseBodyFeature.CompleteAsync();
_state = StreamState.Complete;
}

public override void Flush() => CurrentStream.Flush();
Expand All @@ -127,16 +165,6 @@ public Task SendFileAsync(string path, long offset, long? count, CancellationTok

public override void SetLength(long value) => throw new NotSupportedException();

public Task StartAsync(CancellationToken cancellationToken = default)
{
if (State == StreamState.NotStarted)
{
State = StreamState.Buffering;
}

return _other.StartAsync(cancellationToken);
}

public override void Write(byte[] buffer, int offset, int count) => CurrentStream.Write(buffer, offset, count);

public override void Write(ReadOnlySpan<byte> buffer) => CurrentStream.Write(buffer);
Expand All @@ -148,13 +176,4 @@ public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationTo

public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> CurrentStream.WriteAsync(buffer, offset, count, cancellationToken);

public void ClearContent()
{
if (_bufferedStream is not null)
{
_bufferedStream.Dispose();
_bufferedStream = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO;
using System.Threading.Tasks;

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal interface IBufferedResponseFeature
internal interface IHttpRequestAdapterFeature
{
Stream Stream { get; }
bool IsEnded { get; }

bool SuppressContent { get; set; }

void End();
Task EndAsync();

void ClearContent();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SystemWebAdapters;
using Microsoft.AspNetCore.SystemWebAdapters.Mvc;

namespace Microsoft.Extensions.DependencyInjection;

internal static class MvcExtensions
{
public static ISystemWebAdapterBuilder AddMvc(this ISystemWebAdapterBuilder builder)
{
builder.Services.AddTransient<ResponseEndFilter>();

builder.Services.AddOptions<MvcOptions>()
.Configure(options =>
{
// We want the check for HttpResponse.End() to be done as soon as possible after the action is run.
// This will minimize any chance that output will be written which will fail since the response has completed.
options.Filters.Add<ResponseEndFilter>(int.MaxValue);
});

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.SystemWebAdapters.Mvc;

internal partial class ResponseEndFilter : IActionFilter
{
private readonly ILogger<ResponseEndFilter> _logger;

[LoggerMessage(0, LogLevel.Trace, "Clearing MVC result since HttpResponse.End() was called")]
private partial void LogClearingResult();

public ResponseEndFilter(ILogger<ResponseEndFilter> logger)
{
_logger = logger;
}

public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result is not null && context.HttpContext.Features.Get<IHttpRequestAdapterFeature>() is { IsEnded: true })
{
LogClearingResult();
context.Result = null;
}
}

public void OnActionExecuting(ActionExecutingContext context)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using System.Web.Configuration;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SystemWebAdapters;
using Microsoft.AspNetCore.SystemWebAdapters.Mvc;

namespace Microsoft.Extensions.DependencyInjection;

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

return new SystemWebAdapterBuilder(services);
return new SystemWebAdapterBuilder(services)
.AddMvc();
}

public static void UseSystemWebAdapters(this IApplicationBuilder app)
Expand Down
14 changes: 7 additions & 7 deletions src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class HttpResponse

private NameValueCollection? _headers;
private ResponseHeaders? _typedHeaders;
private IBufferedResponseFeature? _bufferedFeature;
private IHttpRequestAdapterFeature? _adapterFeature;
private TextWriter? _writer;
private HttpCookieCollection? _cookies;

Expand All @@ -34,8 +34,8 @@ internal HttpResponse(HttpResponseCore response)
_response = response;
}

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

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

Expand Down Expand Up @@ -82,8 +82,8 @@ public bool TrySkipIisCustomErrors

public bool SuppressContent
{
get => BufferedFeature.SuppressContent;
set => BufferedFeature.SuppressContent = value;
get => AdapterFeature.SuppressContent;
set => AdapterFeature.SuppressContent = value;
}

public Encoding ContentEncoding
Expand Down Expand Up @@ -201,7 +201,7 @@ private void Redirect(string url, bool endResponse, bool permanent)

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

public void End() => BufferedFeature.End();
public void End() => AdapterFeature.EndAsync().GetAwaiter().GetResult();

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

Expand Down Expand Up @@ -233,7 +233,7 @@ public void ClearContent()
}
else
{
BufferedFeature.ClearContent();
AdapterFeature.ClearContent();
}
}

Expand Down
Loading