Skip to content

Commit ec12fab

Browse files
committed
Spike of unified response body features #11305
1 parent 010ffe6 commit ec12fab

23 files changed

+403
-282
lines changed

src/Hosting/TestHost/src/HttpContextBuilder.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,13 @@ internal HttpContextBuilder(ApplicationWrapper application, bool allowSynchronou
4545
_responseReaderStream = new ResponseBodyReaderStream(pipe, ClientInitiatedAbort, () => _responseReadCompleteCallback?.Invoke(_httpContext));
4646
_responsePipeWriter = new ResponseBodyPipeWriter(pipe, ReturnResponseMessageAsync);
4747
_responseFeature.Body = new ResponseBodyWriterStream(_responsePipeWriter, () => AllowSynchronousIO);
48-
_responseFeature.BodySnapshot = _responseFeature.Body;
4948
_responseFeature.BodyWriter = _responsePipeWriter;
5049

5150
_httpContext.Features.Set<IHttpBodyControlFeature>(this);
5251
_httpContext.Features.Set<IHttpResponseFeature>(_responseFeature);
53-
_httpContext.Features.Set<IHttpResponseStartFeature>(_responseFeature);
52+
_httpContext.Features.Set<IHttpResponseBodyFeature>(_responseFeature);
5453
_httpContext.Features.Set<IHttpRequestLifetimeFeature>(_requestLifetimeFeature);
5554
_httpContext.Features.Set<IHttpResponseTrailersFeature>(_responseTrailersFeature);
56-
_httpContext.Features.Set<IResponseBodyPipeFeature>(_responseFeature);
5755
}
5856

5957
public bool AllowSynchronousIO { get; set; }
@@ -183,6 +181,7 @@ internal async Task ReturnResponseMessageAsync()
183181
Body = _responseReaderStream
184182
};
185183
newFeatures.Set<IHttpResponseFeature>(clientResponseFeature);
184+
newFeatures.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(_responseReaderStream));
186185
_responseTcs.TrySetResult(new DefaultHttpContext(newFeatures));
187186
}
188187
}

src/Hosting/TestHost/src/ResponseFeature.cs

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
namespace Microsoft.AspNetCore.TestHost
1313
{
14-
internal class ResponseFeature : IHttpResponseFeature, IHttpResponseStartFeature, IResponseBodyPipeFeature
14+
internal class ResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature
1515
{
1616
private readonly HeaderDictionary _headers = new HeaderDictionary();
1717
private readonly Action<Exception> _abort;
@@ -24,7 +24,6 @@ internal class ResponseFeature : IHttpResponseFeature, IHttpResponseStartFeature
2424
public ResponseFeature(Action<Exception> abort)
2525
{
2626
Headers = _headers;
27-
Body = new MemoryStream();
2827

2928
// 200 is the default status code all the way down to the host, so we set it
3029
// here to be consistent with the rest of the hosts when writing tests.
@@ -68,29 +67,9 @@ public string ReasonPhrase
6867

6968
public Stream Body { get; set; }
7069

71-
internal Stream BodySnapshot { get; set; }
72-
7370
internal PipeWriter BodyWriter { get; set; }
7471

75-
public PipeWriter Writer
76-
{
77-
get
78-
{
79-
if (!ReferenceEquals(BodySnapshot, Body))
80-
{
81-
BodySnapshot = Body;
82-
BodyWriter = PipeWriter.Create(Body);
83-
84-
OnCompleted((self) =>
85-
{
86-
((PipeWriter)self).Complete();
87-
return Task.CompletedTask;
88-
}, BodyWriter);
89-
}
90-
91-
return BodyWriter;
92-
}
93-
}
72+
public PipeWriter Writer => BodyWriter;
9473

9574
public bool HasStarted { get; set; }
9675

@@ -158,5 +137,19 @@ public async Task StartAsync(CancellationToken token = default)
158137
throw;
159138
}
160139
}
140+
141+
public void DisableResponseBuffering()
142+
{
143+
}
144+
145+
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
146+
{
147+
throw new NotImplementedException();
148+
}
149+
150+
public Task CompleteAsync()
151+
{
152+
return Writer.CompleteAsync().AsTask();
153+
}
161154
}
162155
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.IO;
5+
using System.IO.Pipelines;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNetCore.Http.Features
10+
{
11+
/// <summary>
12+
/// An aggregate of the different ways to interact with the response body.
13+
/// </summary>
14+
public interface IHttpResponseBodyFeature
15+
{
16+
/// <summary>
17+
/// The <see cref="Stream"/> for writing the response body.
18+
/// </summary>
19+
Stream Body { get; }
20+
21+
/// <summary>
22+
/// A <see cref="PipeWriter"/> representing the response body, if any.
23+
/// </summary>
24+
PipeWriter Writer { get; }
25+
26+
/// <summary>
27+
/// Opts out of write buffering for the response.
28+
/// </summary>
29+
void DisableResponseBuffering();
30+
31+
/// <summary>
32+
/// Starts the response by calling OnStarting() and making headers unmodifiable.
33+
/// </summary>
34+
Task StartAsync(CancellationToken token = default);
35+
36+
/// <summary>
37+
/// Sends the requested file in the response body. This may bypass the IHttpResponseFeature.Body
38+
/// <see cref="Stream"/>. A response may include multiple writes.
39+
/// </summary>
40+
/// <param name="path">The full disk path to the file.</param>
41+
/// <param name="offset">The offset in the file to start at.</param>
42+
/// <param name="count">The number of bytes to send, or null to send the remainder of the file.</param>
43+
/// <param name="cancellation">A <see cref="CancellationToken"/> used to abort the transmission.</param>
44+
/// <returns></returns>
45+
Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation);
46+
47+
/// <summary>
48+
/// Flush any remaining response headers, data, or trailers.
49+
/// This may throw if the response is in an invalid state such as a Content-Length mismatch.
50+
/// </summary>
51+
/// <returns></returns>
52+
Task CompleteAsync();
53+
}
54+
}

src/Http/Http.Features/src/IHttpResponseFeature.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public interface IHttpResponseFeature
3030
/// <summary>
3131
/// The <see cref="Stream"/> for writing the response body.
3232
/// </summary>
33-
Stream Body { get; set; }
33+
Stream Body { get; [Obsolete("Use IHttpResponseBodyFeature instead.", error: false)] set; }
3434

3535
/// <summary>
3636
/// Indicates if the response has started. If true, the <see cref="StatusCode"/>,

src/Http/Http.Features/src/IHttpResponseStartFeature.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using System.Threading;
56
using System.Threading.Tasks;
67

@@ -9,6 +10,7 @@ namespace Microsoft.AspNetCore.Http.Features
910
/// <summary>
1011
/// Feature to start response writing.
1112
/// </summary>
13+
[Obsolete("TODO: Remove, use IHttpResponseBodyFeature instead", error: false)]
1214
public interface IHttpResponseStartFeature
1315
{
1416
/// <summary>

src/Http/Http.Features/src/IResponseBodyPipeFeature.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Microsoft.AspNetCore.Http.Features
99
/// <summary>
1010
/// Represents the HttpResponseBody as a PipeWriter
1111
/// </summary>
12+
[Obsolete("TODO: Remove, use IHttpResponseBodyFeature instead.", error: false)]
1213
public interface IResponseBodyPipeFeature
1314
{
1415
/// <summary>

src/Http/Http/src/DefaultHttpContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.ComponentModel;
7+
using System.IO;
78
using System.Security.Claims;
89
using System.Threading;
910
using Microsoft.AspNetCore.Http.Features;
@@ -36,6 +37,7 @@ public DefaultHttpContext()
3637
{
3738
Features.Set<IHttpRequestFeature>(new HttpRequestFeature());
3839
Features.Set<IHttpResponseFeature>(new HttpResponseFeature());
40+
Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
3941
}
4042

4143
public DefaultHttpContext(IFeatureCollection features)

src/Http/Http/src/Features/ResponseBodyPipeFeature.cs

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/Http/Http/src/Internal/DefaultHttpResponse.cs

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ internal sealed class DefaultHttpResponse : HttpResponse
1515
{
1616
// Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
1717
private readonly static Func<IFeatureCollection, IHttpResponseFeature> _nullResponseFeature = f => null;
18-
private readonly static Func<IFeatureCollection, IHttpResponseStartFeature> _nullResponseStartFeature = f => null;
18+
private readonly static Func<IFeatureCollection, IHttpResponseBodyFeature> _nullResponseBodyFeature = f => null;
1919
private readonly static Func<IFeatureCollection, IResponseCookiesFeature> _newResponseCookiesFeature = f => new ResponseCookiesFeature(f);
20-
private readonly static Func<HttpContext, IResponseBodyPipeFeature> _newResponseBodyPipeFeature = context => new ResponseBodyPipeFeature(context);
2120

2221
private readonly DefaultHttpContext _context;
2322
private FeatureReferences<FeatureInterfaces> _features;
@@ -46,15 +45,12 @@ public void Uninitialize()
4645
private IHttpResponseFeature HttpResponseFeature =>
4746
_features.Fetch(ref _features.Cache.Response, _nullResponseFeature);
4847

49-
private IHttpResponseStartFeature HttpResponseStartFeature =>
50-
_features.Fetch(ref _features.Cache.ResponseStart, _nullResponseStartFeature);
48+
private IHttpResponseBodyFeature HttpResponseBodyFeature =>
49+
_features.Fetch(ref _features.Cache.ResponseBody, _nullResponseBodyFeature);
5150

5251
private IResponseCookiesFeature ResponseCookiesFeature =>
5352
_features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature);
5453

55-
private IResponseBodyPipeFeature ResponseBodyPipeFeature =>
56-
_features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newResponseBodyPipeFeature);
57-
5854
public override HttpContext HttpContext { get { return _context; } }
5955

6056
public override int StatusCode
@@ -70,8 +66,13 @@ public override IHeaderDictionary Headers
7066

7167
public override Stream Body
7268
{
73-
get { return HttpResponseFeature.Body; }
74-
set { HttpResponseFeature.Body = value; }
69+
get { return HttpResponseBodyFeature.Body; }
70+
set
71+
{
72+
var feature = new StreamResponseBodyFeature(value);
73+
OnCompleted(feature.CompleteAsync);
74+
_features.Collection.Set<IHttpResponseBodyFeature>(feature);
75+
}
7576
}
7677

7778
public override long? ContentLength
@@ -111,7 +112,7 @@ public override bool HasStarted
111112

112113
public override PipeWriter BodyWriter
113114
{
114-
get { return ResponseBodyPipeFeature.Writer; }
115+
get { return HttpResponseBodyFeature.Writer; }
115116
}
116117

117118
public override void OnStarting(Func<object, Task> callback, object state)
@@ -155,20 +156,14 @@ public override Task StartAsync(CancellationToken cancellationToken = default)
155156
return Task.CompletedTask;
156157
}
157158

158-
if (HttpResponseStartFeature == null)
159-
{
160-
return HttpResponseFeature.Body.FlushAsync(cancellationToken);
161-
}
162-
163-
return HttpResponseStartFeature.StartAsync(cancellationToken);
159+
return HttpResponseBodyFeature.StartAsync(cancellationToken);
164160
}
165161

166162
struct FeatureInterfaces
167163
{
168164
public IHttpResponseFeature Response;
165+
public IHttpResponseBodyFeature ResponseBody;
169166
public IResponseCookiesFeature Cookies;
170-
public IResponseBodyPipeFeature BodyPipe;
171-
public IHttpResponseStartFeature ResponseStart;
172167
}
173168
}
174169
}

0 commit comments

Comments
 (0)