Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 @@ -14,7 +14,13 @@

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal class HttpResponseAdapterFeature : Stream, IHttpResponseBodyFeature, IHttpResponseBufferingFeature, IHttpResponseEndFeature, IHttpResponseContentFeature
internal class HttpResponseAdapterFeature :
Stream,
IHttpResponseBodyFeature,
IHttpResponseBufferingFeature,
IHttpResponseEndFeature,
IHttpResponseContentFeature,
IHttpResponseFilterFeature
{
private enum StreamState
{
Expand All @@ -31,6 +37,7 @@ private enum StreamState
private StreamState _state;
private Func<FileBufferingWriteStream>? _factory;
private bool _suppressContent;
private Stream? _filter;

public HttpResponseAdapterFeature(IHttpResponseBodyFeature httpResponseBody)
{
Expand Down Expand Up @@ -79,7 +86,17 @@ private async ValueTask FlushInternalAsync()
{
if (_state is StreamState.Buffering && _bufferedStream is not null && !SuppressContent)
{
await _bufferedStream.DrainBufferAsync(_responseBodyFeature.Stream);
if (_filter is { } filter)
{
await _bufferedStream.DrainBufferAsync(filter);
await filter.DisposeAsync();
_filter = null;
}
else
{
await _bufferedStream.DrainBufferAsync(_responseBodyFeature.Stream);
}

await _bufferedStream.DisposeAsync();
_bufferedStream = null;
}
Expand Down Expand Up @@ -126,7 +143,7 @@ private void VerifyBuffering()
{
if (_state != StreamState.Buffering)
{
throw new InvalidOperationException("Can only clear content if response is buffered.");
throw new InvalidOperationException("Response buffering is required");
}

Debug.Assert(_factory is not null);
Expand Down Expand Up @@ -179,6 +196,21 @@ public override long Position
set => throw new NotSupportedException();
}

[AllowNull]
Stream IHttpResponseFilterFeature.Filter
{
get
{
VerifyBuffering();
return _filter ?? _responseBodyFeature.Stream;
}
set
{
VerifyBuffering();
_filter = value;
}
}

private async Task CompleteAsync()
{
if (_state == StreamState.Complete)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ private static DelegateDisposable RegisterResponseFeatures(HttpContextCore conte
context.Features.Set<IHttpResponseBufferingFeature>(adapterFeature);
context.Features.Set<IHttpResponseEndFeature>(adapterFeature);
context.Features.Set<IHttpResponseContentFeature>(adapterFeature);
context.Features.Set<IHttpResponseFilterFeature>(adapterFeature);

context.Response.RegisterForDisposeAsync(adapterFeature);

Expand All @@ -75,6 +76,7 @@ private static DelegateDisposable RegisterResponseFeatures(HttpContextCore conte
context.Features.Set<IHttpResponseBufferingFeature>(null);
context.Features.Set<IHttpResponseEndFeature>(null);
context.Features.Set<IHttpResponseContentFeature>(null);
context.Features.Set<IHttpResponseFilterFeature>(null);
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if NETCOREAPP

using System.Diagnostics.CodeAnalysis;
using System.IO;

namespace Microsoft.AspNetCore.SystemWebAdapters;

internal interface IHttpResponseFilterFeature
{
[AllowNull]
Stream Filter { get; set; }
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ internal HttpResponse() { }
public System.Text.Encoding ContentEncoding { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public string ContentType { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public System.Web.HttpCookieCollection Cookies { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public System.IO.Stream Filter { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public System.Collections.Specialized.NameValueCollection Headers { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public bool HeadersWritten { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public bool IsClientConnected { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
Expand Down Expand Up @@ -497,6 +498,7 @@ public partial class HttpResponseBase
public virtual System.Text.Encoding ContentEncoding { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public virtual string ContentType { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public virtual System.Web.HttpCookieCollection Cookies { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public virtual System.IO.Stream Filter { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public virtual System.Collections.Specialized.NameValueCollection Headers { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public virtual bool HeadersWritten { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public virtual bool IsClientConnected { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
Expand Down Expand Up @@ -532,6 +534,7 @@ public partial class HttpResponseWrapper : System.Web.HttpResponseBase
public override System.Text.Encoding ContentEncoding { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public override string ContentType { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public override System.Web.HttpCookieCollection Cookies { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public override System.IO.Stream Filter { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} set { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public override System.Collections.Specialized.NameValueCollection Headers { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public override bool HeadersWritten { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
public override bool IsClientConnected { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
Expand Down
7 changes: 7 additions & 0 deletions src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ public bool TrySkipIisCustomErrors

public Stream OutputStream => _response.Body;

[AllowNull]
public Stream Filter
{
get => _response.HttpContext.Features.GetRequired<IHttpResponseFilterFeature>().Filter;
set => _response.HttpContext.Features.GetRequired<IHttpResponseFilterFeature>().Filter = value;
}

public HttpCookieCollection Cookies => _cookies ??= new(this);

public void AppendCookie(HttpCookie cookie) => Cookies.Add(cookie);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ public virtual TextWriter Output
set => throw new NotImplementedException();
}

[AllowNull]
public virtual Stream Filter
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}

public virtual HttpCachePolicy Cache => throw new NotImplementedException();

public virtual bool IsClientConnected => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -98,6 +99,13 @@ public override bool TrySkipIisCustomErrors

public override HttpCachePolicy Cache => _response.Cache;

[AllowNull]
public override Stream Filter
{
get => _response.Filter;
set => _response.Filter = value;
}

public override void Write(char ch) => _response.Write(ch);

public override void Write(object obj) => _response.Write(obj);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
Expand Down Expand Up @@ -145,6 +148,53 @@ public async Task ClearContent()
Assert.Equal("part2", result);
}

[Fact]
public async Task FilterInstalled()
{
// Arrange
const string Message = "Hello world!";
var bytes = Encoding.UTF8.GetBytes(Message);

TrackingStream filter = default!;

// Act
var result = await RunAsync(context =>
{
context.Response.Filter = filter = new TrackingStream(context.Response.Filter);
context.Response.OutputStream.Write(bytes);
}, builder => builder.BufferResponseStream());

// Assert
Assert.NotNull(filter);
Assert.Equal(bytes, filter.Bytes);
Assert.Equal(Message, result);
Assert.True(filter.IsDisposed);
}

[Fact]
public async Task FilterUninstalled()
{
// Arrange
const string Message = "Hello world!";
var bytes = Encoding.UTF8.GetBytes(Message);

TrackingStream filter = default!;

// Act
var result = await RunAsync(context =>
{
context.Response.Filter = filter = new TrackingStream(context.Response.Filter);
context.Response.OutputStream.Write(bytes);
context.Response.Filter = null;
}, builder => builder.BufferResponseStream());

// Assert
Assert.NotNull(filter);
Assert.Empty(filter.Bytes);
Assert.Equal(Message, result);
Assert.False(filter.IsDisposed);
}

[Fact]
public async Task MultipleClearContent()
{
Expand Down Expand Up @@ -209,4 +259,62 @@ private static async Task<string> RunAsync(Func<HttpContext, Task> action, Actio
await host.StopAsync();
}
}

private sealed class TrackingStream : Stream
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Is not owned by this instance")]
private readonly Stream _stream;
private readonly List<byte> _list = new();

public TrackingStream(Stream other)
{
_stream = other;
}

public byte[] Bytes => _list.ToArray();

public override bool CanRead => _stream.CanRead;

public override bool CanSeek => _stream.CanSeek;

public override bool CanWrite => _stream.CanWrite;

public override long Length => _stream.Length;

public override long Position { get => _stream.Position; set => _stream.Position = value; }

public override void Flush()
{
_stream.Flush();
}

public override int Read(byte[] buffer, int offset, int count)
{
return _stream.Read(buffer, offset, count);
}

public override long Seek(long offset, SeekOrigin origin)
{
return _stream.Seek(offset, origin);
}

public override void SetLength(long value)
{
_stream.SetLength(value);
}

public override void Write(byte[] buffer, int offset, int count)
{
_list.AddRange(buffer.AsMemory(offset, count).ToArray());
_stream.Write(buffer, offset, count);
}

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
IsDisposed = true;
}

public bool IsDisposed { get; private set; }
}
}