Skip to content

Commit d840772

Browse files
authored
Add support for HttpResponse.Filter (#390)
1 parent 7678dee commit d840772

File tree

7 files changed

+172
-3
lines changed

7 files changed

+172
-3
lines changed

src/Microsoft.AspNetCore.SystemWebAdapters.CoreServices/HttpResponseAdapterFeature.cs

+34-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515
namespace Microsoft.AspNetCore.SystemWebAdapters;
1616

17-
internal class HttpResponseAdapterFeature : Stream, IHttpResponseBodyFeature, IHttpResponseBufferingFeature, IHttpResponseEndFeature, IHttpResponseContentFeature
17+
internal class HttpResponseAdapterFeature :
18+
Stream,
19+
IHttpResponseBodyFeature,
20+
IHttpResponseBufferingFeature,
21+
IHttpResponseEndFeature,
22+
IHttpResponseContentFeature
1823
{
1924
private enum StreamState
2025
{
@@ -31,6 +36,7 @@ private enum StreamState
3136
private StreamState _state;
3237
private Func<FileBufferingWriteStream>? _factory;
3338
private bool _suppressContent;
39+
private Stream? _filter;
3440

3541
public HttpResponseAdapterFeature(IHttpResponseBodyFeature httpResponseBody)
3642
{
@@ -87,7 +93,17 @@ private async ValueTask FlushInternalAsync()
8793
{
8894
if (_state is StreamState.Buffering && _bufferedStream is not null && !SuppressContent)
8995
{
90-
await _bufferedStream.DrainBufferAsync(_responseBodyFeature.Stream);
96+
if (_filter is { } filter)
97+
{
98+
await _bufferedStream.DrainBufferAsync(filter);
99+
await filter.DisposeAsync();
100+
_filter = null;
101+
}
102+
else
103+
{
104+
await _bufferedStream.DrainBufferAsync(_responseBodyFeature.Stream);
105+
}
106+
91107
await _bufferedStream.DisposeAsync();
92108
_bufferedStream = null;
93109
}
@@ -134,7 +150,7 @@ private void VerifyBuffering()
134150
{
135151
if (_state != StreamState.Buffering)
136152
{
137-
throw new InvalidOperationException("Can only clear content if response is buffered.");
153+
throw new InvalidOperationException("Response buffering is required");
138154
}
139155

140156
Debug.Assert(_factory is not null);
@@ -187,6 +203,21 @@ public override long Position
187203
set => throw new NotSupportedException();
188204
}
189205

206+
[AllowNull]
207+
Stream IHttpResponseBufferingFeature.Filter
208+
{
209+
get
210+
{
211+
VerifyBuffering();
212+
return _filter ?? _responseBodyFeature.Stream;
213+
}
214+
set
215+
{
216+
VerifyBuffering();
217+
_filter = value;
218+
}
219+
}
220+
190221
private async Task CompleteAsync()
191222
{
192223
if (_state == StreamState.Complete)

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

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
#if NETCOREAPP
55

6+
using System.Diagnostics.CodeAnalysis;
7+
using System.IO;
68
using System.Threading.Tasks;
79

810
namespace Microsoft.AspNetCore.SystemWebAdapters;
@@ -13,6 +15,9 @@ internal interface IHttpResponseBufferingFeature
1315

1416
ValueTask FlushAsync();
1517

18+
[AllowNull]
19+
Stream Filter { get; set; }
20+
1621
bool IsEnabled { get; }
1722
}
1823

src/Microsoft.AspNetCore.SystemWebAdapters/Generated/Ref.Standard.cs

+3
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ internal HttpResponse() { }
457457
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");} }
458458
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");} }
459459
public System.Web.HttpCookieCollection Cookies { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
460+
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");} }
460461
public System.Collections.Specialized.NameValueCollection Headers { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
461462
public bool HeadersWritten { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
462463
public bool IsClientConnected { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
@@ -500,6 +501,7 @@ public partial class HttpResponseBase
500501
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");} }
501502
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");} }
502503
public virtual System.Web.HttpCookieCollection Cookies { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
504+
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");} }
503505
public virtual System.Collections.Specialized.NameValueCollection Headers { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
504506
public virtual bool HeadersWritten { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
505507
public virtual bool IsClientConnected { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
@@ -536,6 +538,7 @@ public partial class HttpResponseWrapper : System.Web.HttpResponseBase
536538
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");} }
537539
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");} }
538540
public override System.Web.HttpCookieCollection Cookies { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
541+
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");} }
539542
public override System.Collections.Specialized.NameValueCollection Headers { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
540543
public override bool HeadersWritten { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }
541544
public override bool IsClientConnected { get { throw new System.PlatformNotSupportedException("Only supported when running on ASP.NET Core or System.Web");} }

src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponse.cs

+7
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ public bool TrySkipIisCustomErrors
7878

7979
public Stream OutputStream => _response.Body;
8080

81+
[AllowNull]
82+
public Stream Filter
83+
{
84+
get => _response.HttpContext.Features.GetRequired<IHttpResponseBufferingFeature>().Filter;
85+
set => _response.HttpContext.Features.GetRequired<IHttpResponseBufferingFeature>().Filter = value;
86+
}
87+
8188
public HttpCookieCollection Cookies => _cookies ??= new(this);
8289

8390
public void AppendCookie(HttpCookie cookie) => Cookies.Add(cookie);

src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseBase.cs

+7
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ public virtual TextWriter Output
8989
set => throw new NotImplementedException();
9090
}
9191

92+
[AllowNull]
93+
public virtual Stream Filter
94+
{
95+
get => throw new NotImplementedException();
96+
set => throw new NotImplementedException();
97+
}
98+
9299
public virtual HttpCachePolicy Cache => throw new NotImplementedException();
93100

94101
public virtual bool IsClientConnected => throw new NotImplementedException();

src/Microsoft.AspNetCore.SystemWebAdapters/HttpResponseWrapper.cs

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Specialized;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.IO;
67
using System.Text;
78
using Microsoft.AspNetCore.Http;
@@ -100,6 +101,13 @@ public override bool TrySkipIisCustomErrors
100101

101102
public override HttpCachePolicy Cache => _response.Cache;
102103

104+
[AllowNull]
105+
public override Stream Filter
106+
{
107+
get => _response.Filter;
108+
set => _response.Filter = value;
109+
}
110+
103111
public override void Write(char ch) => _response.Write(ch);
104112

105113
public override void Write(object obj) => _response.Write(obj);

test/Microsoft.AspNetCore.SystemWebAdapters.CoreServices.Tests/ResponseStreamTests.cs

+108
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Text;
58
using System.Threading.Tasks;
69
using Microsoft.AspNetCore.Builder;
710
using Microsoft.AspNetCore.Hosting;
@@ -146,6 +149,53 @@ public async Task ClearContent()
146149
Assert.Equal("part2", result);
147150
}
148151

152+
[Fact]
153+
public async Task FilterInstalled()
154+
{
155+
// Arrange
156+
const string Message = "Hello world!";
157+
var bytes = Encoding.UTF8.GetBytes(Message);
158+
159+
TrackingStream filter = default!;
160+
161+
// Act
162+
var result = await RunAsync(context =>
163+
{
164+
context.Response.Filter = filter = new TrackingStream(context.Response.Filter);
165+
context.Response.OutputStream.Write(bytes);
166+
}, builder => builder.BufferResponseStream());
167+
168+
// Assert
169+
Assert.NotNull(filter);
170+
Assert.Equal(bytes, filter.Bytes);
171+
Assert.Equal(Message, result);
172+
Assert.True(filter.IsDisposed);
173+
}
174+
175+
[Fact]
176+
public async Task FilterUninstalled()
177+
{
178+
// Arrange
179+
const string Message = "Hello world!";
180+
var bytes = Encoding.UTF8.GetBytes(Message);
181+
182+
TrackingStream filter = default!;
183+
184+
// Act
185+
var result = await RunAsync(context =>
186+
{
187+
context.Response.Filter = filter = new TrackingStream(context.Response.Filter);
188+
context.Response.OutputStream.Write(bytes);
189+
context.Response.Filter = null;
190+
}, builder => builder.BufferResponseStream());
191+
192+
// Assert
193+
Assert.NotNull(filter);
194+
Assert.Empty(filter.Bytes);
195+
Assert.Equal(Message, result);
196+
Assert.False(filter.IsDisposed);
197+
}
198+
149199
[Fact]
150200
public async Task MultipleClearContent()
151201
{
@@ -232,4 +282,62 @@ private static async Task<string> RunAsync(Func<HttpContext, Task> action, Actio
232282
await host.StopAsync();
233283
}
234284
}
285+
286+
private sealed class TrackingStream : Stream
287+
{
288+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Is not owned by this instance")]
289+
private readonly Stream _stream;
290+
private readonly List<byte> _list = new();
291+
292+
public TrackingStream(Stream other)
293+
{
294+
_stream = other;
295+
}
296+
297+
public byte[] Bytes => _list.ToArray();
298+
299+
public override bool CanRead => _stream.CanRead;
300+
301+
public override bool CanSeek => _stream.CanSeek;
302+
303+
public override bool CanWrite => _stream.CanWrite;
304+
305+
public override long Length => _stream.Length;
306+
307+
public override long Position { get => _stream.Position; set => _stream.Position = value; }
308+
309+
public override void Flush()
310+
{
311+
_stream.Flush();
312+
}
313+
314+
public override int Read(byte[] buffer, int offset, int count)
315+
{
316+
return _stream.Read(buffer, offset, count);
317+
}
318+
319+
public override long Seek(long offset, SeekOrigin origin)
320+
{
321+
return _stream.Seek(offset, origin);
322+
}
323+
324+
public override void SetLength(long value)
325+
{
326+
_stream.SetLength(value);
327+
}
328+
329+
public override void Write(byte[] buffer, int offset, int count)
330+
{
331+
_list.AddRange(buffer.AsMemory(offset, count).ToArray());
332+
_stream.Write(buffer, offset, count);
333+
}
334+
335+
protected override void Dispose(bool disposing)
336+
{
337+
base.Dispose(disposing);
338+
IsDisposed = true;
339+
}
340+
341+
public bool IsDisposed { get; private set; }
342+
}
235343
}

0 commit comments

Comments
 (0)