Unified response body features (#12328)

This commit is contained in:
Chris Ross 2019-07-25 16:27:08 -07:00 committed by GitHub
parent 2884ef6e1f
commit 080660967b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1217 additions and 1184 deletions

View File

@ -45,15 +45,13 @@ namespace Microsoft.AspNetCore.TestHost
_responseReaderStream = new ResponseBodyReaderStream(pipe, ClientInitiatedAbort, () => _responseReadCompleteCallback?.Invoke(_httpContext));
_responsePipeWriter = new ResponseBodyPipeWriter(pipe, ReturnResponseMessageAsync);
_responseFeature.Body = new ResponseBodyWriterStream(_responsePipeWriter, () => AllowSynchronousIO);
_responseFeature.BodySnapshot = _responseFeature.Body;
_responseFeature.BodyWriter = _responsePipeWriter;
_httpContext.Features.Set<IHttpBodyControlFeature>(this);
_httpContext.Features.Set<IHttpResponseFeature>(_responseFeature);
_httpContext.Features.Set<IHttpResponseStartFeature>(_responseFeature);
_httpContext.Features.Set<IHttpResponseBodyFeature>(_responseFeature);
_httpContext.Features.Set<IHttpRequestLifetimeFeature>(_requestLifetimeFeature);
_httpContext.Features.Set<IHttpResponseTrailersFeature>(_responseTrailersFeature);
_httpContext.Features.Set<IResponseBodyPipeFeature>(_responseFeature);
}
public bool AllowSynchronousIO { get; set; }
@ -183,6 +181,7 @@ namespace Microsoft.AspNetCore.TestHost
Body = _responseReaderStream
};
newFeatures.Set<IHttpResponseFeature>(clientResponseFeature);
newFeatures.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(_responseReaderStream));
_responseTcs.TrySetResult(new DefaultHttpContext(newFeatures));
}
}

View File

@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.TestHost
{
internal class ResponseFeature : IHttpResponseFeature, IHttpResponseStartFeature, IResponseBodyPipeFeature
internal class ResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature
{
private readonly HeaderDictionary _headers = new HeaderDictionary();
private readonly Action<Exception> _abort;
@ -24,7 +24,6 @@ namespace Microsoft.AspNetCore.TestHost
public ResponseFeature(Action<Exception> abort)
{
Headers = _headers;
Body = new MemoryStream();
// 200 is the default status code all the way down to the host, so we set it
// here to be consistent with the rest of the hosts when writing tests.
@ -68,29 +67,11 @@ namespace Microsoft.AspNetCore.TestHost
public Stream Body { get; set; }
internal Stream BodySnapshot { get; set; }
public Stream Stream => Body;
internal PipeWriter BodyWriter { get; set; }
public PipeWriter Writer
{
get
{
if (!ReferenceEquals(BodySnapshot, Body))
{
BodySnapshot = Body;
BodyWriter = PipeWriter.Create(Body);
OnCompleted((self) =>
{
((PipeWriter)self).Complete();
return Task.CompletedTask;
}, BodyWriter);
}
return BodyWriter;
}
}
public PipeWriter Writer => BodyWriter;
public bool HasStarted { get; set; }
@ -158,5 +139,19 @@ namespace Microsoft.AspNetCore.TestHost
throw;
}
}
public void DisableBuffering()
{
}
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellation);
}
public Task CompleteAsync()
{
return Writer.CompleteAsync().AsTask();
}
}
}

View File

@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.TestHost
{
var handler = new ClientHandler(new PathString("/A/Path/"), new InspectingApplication(features =>
{
// TODO: Assert.True(context.RequestAborted.CanBeCanceled);
Assert.True(features.Get<IHttpRequestLifetimeFeature>().RequestAborted.CanBeCanceled);
Assert.Equal("HTTP/1.1", features.Get<IHttpRequestFeature>().Protocol);
Assert.Equal("GET", features.Get<IHttpRequestFeature>().Method);
Assert.Equal("https", features.Get<IHttpRequestFeature>().Scheme);
@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.TestHost
Assert.NotNull(features.Get<IHttpRequestFeature>().Body);
Assert.NotNull(features.Get<IHttpRequestFeature>().Headers);
Assert.NotNull(features.Get<IHttpResponseFeature>().Headers);
Assert.NotNull(features.Get<IHttpResponseFeature>().Body);
Assert.NotNull(features.Get<IHttpResponseBodyFeature>().Stream);
Assert.Equal(200, features.Get<IHttpResponseFeature>().StatusCode);
Assert.Null(features.Get<IHttpResponseFeature>().ReasonPhrase);
Assert.Equal("example.com", features.Get<IHttpRequestFeature>().Headers["host"]);

View File

@ -282,6 +282,7 @@ namespace Microsoft.AspNetCore.Http
public abstract Microsoft.AspNetCore.Http.IHeaderDictionary Headers { get; }
public abstract Microsoft.AspNetCore.Http.HttpContext HttpContext { get; }
public abstract int StatusCode { get; set; }
public virtual System.Threading.Tasks.Task CompleteAsync() { throw null; }
public abstract void OnCompleted(System.Func<object, System.Threading.Tasks.Task> callback, object state);
public virtual void OnCompleted(System.Func<System.Threading.Tasks.Task> callback) { }
public abstract void OnStarting(System.Func<object, System.Threading.Tasks.Task> callback, object state);

View File

@ -127,9 +127,13 @@ namespace Microsoft.AspNetCore.Http
/// Starts the response by calling OnStarting() and making headers unmodifiable.
/// </summary>
/// <param name="cancellationToken"></param>
/// <remarks>
/// If the <see cref="IHttpResponseStartFeature"/> isn't set, StartAsync will default to calling HttpResponse.Body.FlushAsync().
/// </remarks>
public virtual Task StartAsync(CancellationToken cancellationToken = default) { throw new NotImplementedException(); }
/// <summary>
/// Flush any remaining response headers, data, or trailers.
/// This may throw if the response is in an invalid state such as a Content-Length mismatch.
/// </summary>
/// <returns></returns>
public virtual Task CompleteAsync() { throw new NotImplementedException(); }
}
}

View File

@ -55,7 +55,6 @@ namespace Microsoft.AspNetCore.Http.Extensions
}
public static partial class StreamCopyOperation
{
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task CopyToAsync(System.IO.Stream source, System.IO.Stream destination, long? count, int bufferSize, System.Threading.CancellationToken cancel) { throw null; }
public static System.Threading.Tasks.Task CopyToAsync(System.IO.Stream source, System.IO.Stream destination, long? count, System.Threading.CancellationToken cancel) { throw null; }
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core common extension methods for HTTP abstractions, HTTP headers, HTTP request/response, and session state.</Description>
@ -9,6 +9,10 @@
<PackageTags>aspnetcore</PackageTags>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" Link="StreamCopyOperationInternal.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Net.Http.Headers" />

View File

@ -128,43 +128,10 @@ namespace Microsoft.AspNetCore.Http
private static Task SendFileAsyncCore(HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default)
{
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
if (sendFile == null)
{
return SendFileAsyncCore(response.Body, fileName, offset, count, cancellationToken);
}
var sendFile = response.HttpContext.Features.Get<IHttpResponseBodyFeature>();
return sendFile.SendFileAsync(fileName, offset, count, cancellationToken);
}
// Not safe for overlapped writes.
private static async Task SendFileAsyncCore(Stream outputStream, string fileName, long offset, long? count, CancellationToken cancel = default)
{
cancel.ThrowIfCancellationRequested();
var fileInfo = new FileInfo(fileName);
CheckRange(offset, count, fileInfo.Length);
int bufferSize = 1024 * 16;
var fileStream = new FileStream(
fileName,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: bufferSize,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
using (fileStream)
{
if (offset > 0)
{
fileStream.Seek(offset, SeekOrigin.Begin);
}
await StreamCopyOperation.CopyToAsync(fileStream, outputStream, count, cancel);
}
}
private static void CheckRange(long offset, long? count, long fileLength)
{
if (offset < 0 || offset > fileLength)
@ -178,4 +145,4 @@ namespace Microsoft.AspNetCore.Http
}
}
}
}
}

View File

@ -1,9 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -13,75 +10,23 @@ namespace Microsoft.AspNetCore.Http.Extensions
// FYI: In most cases the source will be a FileStream and the destination will be to the network.
public static class StreamCopyOperation
{
private const int DefaultBufferSize = 4096;
/// <summary>Asynchronously reads the bytes from the source stream and writes them to another stream.</summary>
/// <summary>Asynchronously reads the given number of bytes from the source stream and writes them to another stream.</summary>
/// <returns>A task that represents the asynchronous copy operation.</returns>
/// <param name="source">The stream from which the contents will be copied.</param>
/// <param name="destination">The stream to which the contents of the current stream will be copied.</param>
/// <param name="count">The count of bytes to be copied.</param>
/// <param name="cancel">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel)
{
return CopyToAsync(source, destination, count, DefaultBufferSize, cancel);
}
=> StreamCopyOperationInternal.CopyToAsync(source, destination, count, cancel);
/// <summary>Asynchronously reads the bytes from the source stream and writes them to another stream, using a specified buffer size.</summary>
/// <summary>Asynchronously reads the given number of bytes from the source stream and writes them to another stream, using a specified buffer size.</summary>
/// <returns>A task that represents the asynchronous copy operation.</returns>
/// <param name="source">The stream from which the contents will be copied.</param>
/// <param name="destination">The stream to which the contents of the current stream will be copied.</param>
/// <param name="count">The count of bytes to be copied.</param>
/// <param name="bufferSize">The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096.</param>
/// <param name="cancel">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
public static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel)
{
long? bytesRemaining = count;
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
Debug.Assert(source != null);
Debug.Assert(destination != null);
Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0);
Debug.Assert(buffer != null);
while (true)
{
// The natural end of the range.
if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0)
{
return;
}
cancel.ThrowIfCancellationRequested();
int readLength = buffer.Length;
if (bytesRemaining.HasValue)
{
readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength);
}
int read = await source.ReadAsync(buffer, 0, readLength, cancel);
if (bytesRemaining.HasValue)
{
bytesRemaining -= read;
}
// End of the source stream.
if (read == 0)
{
return;
}
cancel.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read, cancel);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public static Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel)
=> StreamCopyOperationInternal.CopyToAsync(source, destination, count, bufferSize, cancel);
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information.
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
@ -22,8 +23,8 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests
{
var context = new DefaultHttpContext();
var response = context.Response;
var fakeFeature = new FakeSendFileFeature();
context.Features.Set<IHttpSendFileFeature>(fakeFeature);
var fakeFeature = new FakeResponseBodyFeature();
context.Features.Set<IHttpResponseBodyFeature>(fakeFeature);
await response.SendFileAsync("bob", 1, 3, CancellationToken.None);
@ -33,13 +34,27 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests
Assert.Equal(CancellationToken.None, fakeFeature.token);
}
private class FakeSendFileFeature : IHttpSendFileFeature
private class FakeResponseBodyFeature : IHttpResponseBodyFeature
{
public string name = null;
public long offset = 0;
public long? length = null;
public CancellationToken token;
public Stream Stream => throw new System.NotImplementedException();
public PipeWriter Writer => throw new System.NotImplementedException();
public Task CompleteAsync()
{
throw new System.NotImplementedException();
}
public void DisableBuffering()
{
throw new System.NotImplementedException();
}
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
this.name = path;
@ -48,6 +63,11 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests
this.token = cancellation;
return Task.FromResult(0);
}
public Task StartAsync(CancellationToken token = default)
{
throw new System.NotImplementedException();
}
}
}
}

View File

@ -156,6 +156,7 @@ namespace Microsoft.AspNetCore.Http.Features
{
bool AllowSynchronousIO { get; set; }
}
[System.ObsoleteAttribute("See IHttpRequestBodyFeature or IHttpResponseBodyFeature DisableBuffering", true)]
public partial interface IHttpBufferingFeature
{
void DisableRequestBuffering();
@ -204,12 +205,18 @@ namespace Microsoft.AspNetCore.Http.Features
{
void Reset(int errorCode);
}
public partial interface IHttpResponseCompletionFeature
public partial interface IHttpResponseBodyFeature
{
System.IO.Stream Stream { get; }
System.IO.Pipelines.PipeWriter Writer { get; }
System.Threading.Tasks.Task CompleteAsync();
void DisableBuffering();
System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken));
}
public partial interface IHttpResponseFeature
{
[System.ObsoleteAttribute("Use IHttpResponseBodyFeature.Stream instead.", false)]
System.IO.Stream Body { get; set; }
bool HasStarted { get; }
Microsoft.AspNetCore.Http.IHeaderDictionary Headers { get; set; }
@ -218,10 +225,6 @@ namespace Microsoft.AspNetCore.Http.Features
void OnCompleted(System.Func<object, System.Threading.Tasks.Task> callback, object state);
void OnStarting(System.Func<object, System.Threading.Tasks.Task> callback, object state);
}
public partial interface IHttpResponseStartFeature
{
System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken token = default(System.Threading.CancellationToken));
}
public partial interface IHttpResponseTrailersFeature
{
Microsoft.AspNetCore.Http.IHeaderDictionary Trailers { get; set; }
@ -230,6 +233,7 @@ namespace Microsoft.AspNetCore.Http.Features
{
Microsoft.AspNetCore.Http.Features.HttpsCompressionMode Mode { get; set; }
}
[System.ObsoleteAttribute("Use IHttpResponseBodyFeature instead.", true)]
public partial interface IHttpSendFileFeature
{
System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellation);
@ -260,10 +264,6 @@ namespace Microsoft.AspNetCore.Http.Features
{
Microsoft.AspNetCore.Http.IRequestCookieCollection Cookies { get; set; }
}
public partial interface IResponseBodyPipeFeature
{
System.IO.Pipelines.PipeWriter Writer { get; }
}
public partial interface IResponseCookiesFeature
{
Microsoft.AspNetCore.Http.IResponseCookies Cookies { get; }

View File

@ -1,8 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Http.Features
{
[Obsolete("See IHttpRequestBodyFeature or IHttpResponseBodyFeature DisableBuffering", error: true)]
public interface IHttpBufferingFeature
{
void DisableRequestBuffering();

View File

@ -0,0 +1,53 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// An aggregate of the different ways to interact with the response body.
/// </summary>
public interface IHttpResponseBodyFeature
{
/// <summary>
/// The <see cref="System.IO.Stream"/> for writing the response body.
/// </summary>
Stream Stream { get; }
/// <summary>
/// A <see cref="PipeWriter"/> representing the response body, if any.
/// </summary>
PipeWriter Writer { get; }
/// <summary>
/// Opts out of write buffering for the response.
/// </summary>
void DisableBuffering();
/// <summary>
/// Starts the response by calling OnStarting() and making headers unmodifiable.
/// </summary>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Sends the requested file in the response body. A response may include multiple writes.
/// </summary>
/// <param name="path">The full disk path to the file.</param>
/// <param name="offset">The offset in the file to start at.</param>
/// <param name="count">The number of bytes to send, or null to send the remainder of the file.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to abort the transmission.</param>
/// <returns></returns>
Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default);
/// <summary>
/// Flush any remaining response headers, data, or trailers.
/// This may throw if the response is in an invalid state such as a Content-Length mismatch.
/// </summary>
/// <returns></returns>
Task CompleteAsync();
}
}

View File

@ -1,20 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// A feature to gracefully end a response.
/// </summary>
public interface IHttpResponseCompletionFeature
{
/// <summary>
/// Flush any remaining response headers, data, or trailers.
/// This may throw if the response is in an invalid state such as a Content-Length mismatch.
/// </summary>
/// <returns></returns>
Task CompleteAsync();
}
}

View File

@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Http.Features
/// <summary>
/// The <see cref="Stream"/> for writing the response body.
/// </summary>
[Obsolete("Use IHttpResponseBodyFeature.Stream instead.", error: false)]
Stream Body { get; set; }
/// <summary>

View File

@ -1,19 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// Feature to start response writing.
/// </summary>
public interface IHttpResponseStartFeature
{
/// <summary>
/// Starts the response by calling OnStarting() and making headers unmodifiable.
/// </summary>
Task StartAsync(CancellationToken token = default);
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -10,6 +11,7 @@ namespace Microsoft.AspNetCore.Http.Features
/// <summary>
/// Provides an efficient mechanism for transferring files from disk to the network.
/// </summary>
[Obsolete("Use IHttpResponseBodyFeature instead.", error: true)]
public interface IHttpSendFileFeature
{
/// <summary>
@ -23,4 +25,4 @@ namespace Microsoft.AspNetCore.Http.Features
/// <returns></returns>
Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation);
}
}
}

View File

@ -1,19 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO.Pipelines;
namespace Microsoft.AspNetCore.Http.Features
{
/// <summary>
/// Represents the HttpResponseBody as a PipeWriter
/// </summary>
public interface IResponseBodyPipeFeature
{
/// <summary>
/// A <see cref="PipeWriter"/> representing the response body, if any.
/// </summary>
PipeWriter Writer { get; }
}
}

View File

@ -195,6 +195,26 @@ namespace Microsoft.AspNetCore.Http
{
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Http.IFormCollection> ReadFormAsync(this Microsoft.AspNetCore.Http.HttpRequest request, Microsoft.AspNetCore.Http.Features.FormOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public static partial class SendFileFallback
{
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task SendFileAsync(System.IO.Stream destination, string filePath, long offset, long? count, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial class StreamResponseBodyFeature : Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature
{
public StreamResponseBodyFeature(System.IO.Stream stream) { }
public StreamResponseBodyFeature(System.IO.Stream stream, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature priorFeature) { }
public Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature PriorFeature { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.IO.Stream Stream { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public System.IO.Pipelines.PipeWriter Writer { get { throw null; } }
[System.Diagnostics.DebuggerStepThroughAttribute]
public virtual System.Threading.Tasks.Task CompleteAsync() { throw null; }
public virtual void DisableBuffering() { }
public void Dispose() { }
[System.Diagnostics.DebuggerStepThroughAttribute]
public virtual System.Threading.Tasks.Task SendFileAsync(string path, long offset, long? count, System.Threading.CancellationToken cancellationToken) { throw null; }
public virtual System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
}
namespace Microsoft.AspNetCore.Http.Features
{
@ -305,11 +325,6 @@ namespace Microsoft.AspNetCore.Http.Features
public void Dispose() { }
public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
}
public partial class ResponseBodyPipeFeature : Microsoft.AspNetCore.Http.Features.IResponseBodyPipeFeature
{
public ResponseBodyPipeFeature(Microsoft.AspNetCore.Http.HttpContext context) { }
public System.IO.Pipelines.PipeWriter Writer { get { throw null; } }
}
public partial class ResponseCookiesFeature : Microsoft.AspNetCore.Http.Features.IResponseCookiesFeature
{
public ResponseCookiesFeature(Microsoft.AspNetCore.Http.Features.IFeatureCollection features) { }

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Security.Claims;
using System.Threading;
using Microsoft.AspNetCore.Http.Features;
@ -36,6 +37,7 @@ namespace Microsoft.AspNetCore.Http
{
Features.Set<IHttpRequestFeature>(new HttpRequestFeature());
Features.Set<IHttpResponseFeature>(new HttpResponseFeature());
Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
}
public DefaultHttpContext(IFeatureCollection features)

View File

@ -1,47 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http.Features
{
public class ResponseBodyPipeFeature : IResponseBodyPipeFeature
{
private PipeWriter _internalPipeWriter;
private Stream _streamInstanceWhenWrapped;
private HttpContext _context;
public ResponseBodyPipeFeature(HttpContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
_context = context;
}
public PipeWriter Writer
{
get
{
if (_internalPipeWriter == null ||
!ReferenceEquals(_streamInstanceWhenWrapped, _context.Response.Body))
{
_streamInstanceWhenWrapped = _context.Response.Body;
_internalPipeWriter = PipeWriter.Create(_context.Response.Body);
_context.Response.OnCompleted((self) =>
{
((PipeWriter)self).Complete();
return Task.CompletedTask;
}, _internalPipeWriter);
}
return _internalPipeWriter;
}
}
}
}

View File

@ -15,9 +15,8 @@ namespace Microsoft.AspNetCore.Http
{
// Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
private readonly static Func<IFeatureCollection, IHttpResponseFeature> _nullResponseFeature = f => null;
private readonly static Func<IFeatureCollection, IHttpResponseStartFeature> _nullResponseStartFeature = f => null;
private readonly static Func<IFeatureCollection, IHttpResponseBodyFeature> _nullResponseBodyFeature = f => null;
private readonly static Func<IFeatureCollection, IResponseCookiesFeature> _newResponseCookiesFeature = f => new ResponseCookiesFeature(f);
private readonly static Func<HttpContext, IResponseBodyPipeFeature> _newResponseBodyPipeFeature = context => new ResponseBodyPipeFeature(context);
private readonly DefaultHttpContext _context;
private FeatureReferences<FeatureInterfaces> _features;
@ -46,15 +45,12 @@ namespace Microsoft.AspNetCore.Http
private IHttpResponseFeature HttpResponseFeature =>
_features.Fetch(ref _features.Cache.Response, _nullResponseFeature);
private IHttpResponseStartFeature HttpResponseStartFeature =>
_features.Fetch(ref _features.Cache.ResponseStart, _nullResponseStartFeature);
private IHttpResponseBodyFeature HttpResponseBodyFeature =>
_features.Fetch(ref _features.Cache.ResponseBody, _nullResponseBodyFeature);
private IResponseCookiesFeature ResponseCookiesFeature =>
_features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature);
private IResponseBodyPipeFeature ResponseBodyPipeFeature =>
_features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newResponseBodyPipeFeature);
public override HttpContext HttpContext { get { return _context; } }
public override int StatusCode
@ -70,8 +66,26 @@ namespace Microsoft.AspNetCore.Http
public override Stream Body
{
get { return HttpResponseFeature.Body; }
set { HttpResponseFeature.Body = value; }
get { return HttpResponseBodyFeature.Stream; }
set
{
var otherFeature = _features.Collection.Get<IHttpResponseBodyFeature>();
if (otherFeature is StreamResponseBodyFeature streamFeature
&& streamFeature.PriorFeature != null
&& object.ReferenceEquals(value, streamFeature.PriorFeature.Stream))
{
// They're reverting the stream back to the prior one. Revert the whole feature.
_features.Collection.Set(streamFeature.PriorFeature);
// CompleteAsync is registered with HttpResponse.OnCompleted and there's no way to unregister it.
// Prevent it from running by marking as disposed.
streamFeature.Dispose();
return;
}
var feature = new StreamResponseBodyFeature(value, otherFeature);
OnCompleted(feature.CompleteAsync);
_features.Collection.Set<IHttpResponseBodyFeature>(feature);
}
}
public override long? ContentLength
@ -111,7 +125,7 @@ namespace Microsoft.AspNetCore.Http
public override PipeWriter BodyWriter
{
get { return ResponseBodyPipeFeature.Writer; }
get { return HttpResponseBodyFeature.Writer; }
}
public override void OnStarting(Func<object, Task> callback, object state)
@ -155,20 +169,16 @@ namespace Microsoft.AspNetCore.Http
return Task.CompletedTask;
}
if (HttpResponseStartFeature == null)
{
return HttpResponseFeature.Body.FlushAsync(cancellationToken);
}
return HttpResponseStartFeature.StartAsync(cancellationToken);
return HttpResponseBodyFeature.StartAsync(cancellationToken);
}
public override Task CompleteAsync() => HttpResponseBodyFeature.CompleteAsync();
struct FeatureInterfaces
{
public IHttpResponseFeature Response;
public IHttpResponseBodyFeature ResponseBody;
public IResponseCookiesFeature Cookies;
public IResponseBodyPipeFeature BodyPipe;
public IHttpResponseStartFeature ResponseStart;
}
}
}

View File

@ -13,6 +13,7 @@
<ItemGroup>
<Compile Include="$(SharedSourceRoot)CopyOnWriteDictionary\*.cs" />
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" />
<Compile Include="..\..\Shared\StreamCopyOperationInternal.cs" Link="Internal\StreamCopyOperationInternal.cs" />
<Compile Include="..\..\WebUtilities\src\AspNetCoreTempDirectory.cs" LinkBase="Internal" />
</ItemGroup>

View File

@ -0,0 +1,54 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http
{
public static class SendFileFallback
{
/// <summary>
/// Copies the segment of the file to the destination stream.
/// </summary>
/// <param name="destination">The stream to write the file segment to.</param>
/// <param name="filePath">The full disk path to the file.</param>
/// <param name="offset">The offset in the file to start at.</param>
/// <param name="count">The number of bytes to send, or null to send the remainder of the file.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to abort the transmission.</param>
/// <returns></returns>
public static async Task SendFileAsync(Stream destination, string filePath, long offset, long? count, CancellationToken cancellationToken)
{
var fileInfo = new FileInfo(filePath);
if (offset < 0 || offset > fileInfo.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if (count.HasValue &&
(count.Value < 0 || count.Value > fileInfo.Length - offset))
{
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
}
cancellationToken.ThrowIfCancellationRequested();
int bufferSize = 1024 * 16;
var fileStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: bufferSize,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
using (fileStream)
{
fileStream.Seek(offset, SeekOrigin.Begin);
await StreamCopyOperationInternal.CopyToAsync(fileStream, destination, count, bufferSize, cancellationToken);
}
}
}
}

View File

@ -0,0 +1,147 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Http
{
/// <summary>
/// An implementation of <see cref="IHttpResponseBodyFeature"/> that aproximates all of the APIs over the given Stream.
/// </summary>
public class StreamResponseBodyFeature : IHttpResponseBodyFeature
{
private PipeWriter _pipeWriter;
private bool _started;
private bool _completed;
private bool _disposed;
/// <summary>
/// Wraps the given stream.
/// </summary>
/// <param name="stream"></param>
public StreamResponseBodyFeature(Stream stream)
{
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
}
/// <summary>
/// Wraps the given stream and tracks the prior feature instance.
/// </summary>
/// <param name="stream"></param>
/// <param name="priorFeature"></param>
public StreamResponseBodyFeature(Stream stream, IHttpResponseBodyFeature priorFeature)
{
Stream = stream ?? throw new ArgumentNullException(nameof(stream));
PriorFeature = priorFeature;
}
/// <summary>
/// The original response body stream.
/// </summary>
public Stream Stream { get; }
/// <summary>
/// The prior feature, if any.
/// </summary>
public IHttpResponseBodyFeature PriorFeature { get; }
/// <summary>
/// A PipeWriter adapted over the given stream.
/// </summary>
public PipeWriter Writer
{
get
{
if (_pipeWriter == null)
{
_pipeWriter = PipeWriter.Create(Stream, new StreamPipeWriterOptions(leaveOpen: true));
if (_completed)
{
_pipeWriter.Complete();
}
}
return _pipeWriter;
}
}
/// <summary>
/// Not supported.
/// </summary>
public virtual void DisableBuffering()
{
}
/// <summary>
/// Copies the specified file segment to the given response stream.
/// This calls StartAsync if it has not previoulsy been called.
/// </summary>
/// <param name="path">The full disk path to the file.</param>
/// <param name="offset">The offset in the file to start at.</param>
/// <param name="count">The number of bytes to send, or null to send the remainder of the file.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> used to abort the transmission.</param>
/// <returns></returns>
public virtual async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken)
{
if (!_started)
{
await StartAsync(cancellationToken);
}
await SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellationToken);
}
/// <summary>
/// Flushes the given stream if this has not previously been called.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public virtual Task StartAsync(CancellationToken cancellationToken = default)
{
if (!_started)
{
_started = true;
return Stream.FlushAsync(cancellationToken);
}
return Task.CompletedTask;
}
/// <summary>
/// This calls StartAsync if it has not previoulsy been called.
/// It will complete the adapted pipe if it exists.
/// </summary>
/// <returns></returns>
public virtual async Task CompleteAsync()
{
// CompleteAsync is registered with HttpResponse.OnCompleted and there's no way to unregister it.
// Prevent it from running by marking as disposed.
if (_disposed)
{
return;
}
if (_completed)
{
return;
}
_completed = true;
if (_pipeWriter != null)
{
await _pipeWriter.CompleteAsync();
}
}
/// <summary>
/// Prevents CompleteAsync from operating.
/// </summary>
public void Dispose()
{
_disposed = true;
}
}
}

View File

@ -158,8 +158,8 @@ namespace Microsoft.AspNetCore.Http
var features = new FeatureCollection();
features.Set<IHttpRequestFeature>(new HttpRequestFeature());
features.Set<IHttpResponseFeature>(new HttpResponseFeature());
features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
features.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature());
features.Set<IHttpResponseStartFeature>(new MockHttpResponseStartFeature());
// FeatureCollection is set. all cached interfaces are null.
var context = new DefaultHttpContext(features);
@ -179,8 +179,8 @@ namespace Microsoft.AspNetCore.Http
var newFeatures = new FeatureCollection();
newFeatures.Set<IHttpRequestFeature>(new HttpRequestFeature());
newFeatures.Set<IHttpResponseFeature>(new HttpResponseFeature());
newFeatures.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
newFeatures.Set<IHttpWebSocketFeature>(new TestHttpWebSocketFeature());
newFeatures.Set<IHttpResponseStartFeature>(new MockHttpResponseStartFeature());
// FeatureCollection is set to newFeatures. all cached interfaces are null.
context.Initialize(newFeatures);
@ -450,14 +450,6 @@ namespace Microsoft.AspNetCore.Http
}
}
private class MockHttpResponseStartFeature : IHttpResponseStartFeature
{
public Task StartAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
}
private class AsyncDisposableServiceProvider : IServiceProvider, IDisposable, IServiceScopeFactory
{
private readonly ServiceProvider _serviceProvider;

View File

@ -1,25 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using Xunit;
namespace Microsoft.AspNetCore.Http.Features
{
public class ResponseBodyPipeFeatureTests
{
[Fact]
public void ResponseBodyReturnsStreamPipeReader()
{
var context = new DefaultHttpContext();
var expectedStream = new MemoryStream();
context.Response.Body = expectedStream;
var feature = new ResponseBodyPipeFeature(context);
var pipeBody = feature.Writer;
Assert.NotNull(pipeBody);
}
}
}

View File

@ -77,7 +77,7 @@ namespace Microsoft.AspNetCore.Http
public async Task ResponseStart_CallsFeatureIfSet()
{
var features = new FeatureCollection();
var mock = new Mock<IHttpResponseStartFeature>();
var mock = new Mock<IHttpResponseBodyFeature>();
mock.Setup(o => o.StartAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
features.Set(mock.Object);
@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Http
{
var features = new FeatureCollection();
var mock = new Mock<IHttpResponseStartFeature>();
var mock = new Mock<IHttpResponseBodyFeature>();
var ct = new CancellationToken();
mock.Setup(o => o.StartAsync(It.Is<CancellationToken>((localCt) => localCt.Equals(ct)))).Returns(Task.CompletedTask);
features.Set(mock.Object);
@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Http
{
var features = new FeatureCollection();
var startMock = new Mock<IHttpResponseStartFeature>();
var startMock = new Mock<IHttpResponseBodyFeature>();
startMock.Setup(o => o.StartAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask);
features.Set(startMock.Object);

View File

@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.Owin
public OwinEnvironmentFeature() { }
public System.Collections.Generic.IDictionary<string, object> Environment { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public partial class OwinFeatureCollection : Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature, Microsoft.AspNetCore.Http.Features.IFeatureCollection, Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature, Microsoft.AspNetCore.Http.Features.IHttpRequestFeature, Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature, Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature, Microsoft.AspNetCore.Http.Features.IHttpResponseFeature, Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature, Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature, Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature, Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, object>>, System.Collections.IEnumerable
public partial class OwinFeatureCollection : Microsoft.AspNetCore.Http.Features.Authentication.IHttpAuthenticationFeature, Microsoft.AspNetCore.Http.Features.IFeatureCollection, Microsoft.AspNetCore.Http.Features.IHttpConnectionFeature, Microsoft.AspNetCore.Http.Features.IHttpRequestFeature, Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature, Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature, Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature, Microsoft.AspNetCore.Http.Features.IHttpResponseFeature, Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature, Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature, Microsoft.AspNetCore.Owin.IOwinEnvironmentFeature, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<System.Type, object>>, System.Collections.IEnumerable
{
public OwinFeatureCollection(System.Collections.Generic.IDictionary<string, object> environment) { }
public System.Collections.Generic.IDictionary<string, object> Environment { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
@ -85,6 +85,8 @@ namespace Microsoft.AspNetCore.Owin
string Microsoft.AspNetCore.Http.Features.IHttpRequestFeature.Scheme { get { throw null; } set { } }
string Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature.TraceIdentifier { get { throw null; } set { } }
System.Threading.CancellationToken Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature.RequestAborted { get { throw null; } set { } }
System.IO.Stream Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.Stream { get { throw null; } }
System.IO.Pipelines.PipeWriter Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.Writer { get { throw null; } }
System.IO.Stream Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.Body { get { throw null; } set { } }
bool Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.HasStarted { get { throw null; } }
Microsoft.AspNetCore.Http.IHeaderDictionary Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.Headers { get { throw null; } set { } }
@ -99,9 +101,13 @@ namespace Microsoft.AspNetCore.Owin
public System.Collections.Generic.IEnumerator<System.Collections.Generic.KeyValuePair<System.Type, object>> GetEnumerator() { throw null; }
public TFeature Get<TFeature>() { throw null; }
void Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature.Abort() { }
System.Threading.Tasks.Task Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.CompleteAsync() { throw null; }
void Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.DisableBuffering() { }
System.Threading.Tasks.Task Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? length, System.Threading.CancellationToken cancellation) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
System.Threading.Tasks.Task Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature.StartAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
void Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.OnCompleted(System.Func<object, System.Threading.Tasks.Task> callback, object state) { }
void Microsoft.AspNetCore.Http.Features.IHttpResponseFeature.OnStarting(System.Func<object, System.Threading.Tasks.Task> callback, object state) { }
System.Threading.Tasks.Task Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, System.Threading.CancellationToken cancellation) { throw null; }
System.Threading.Tasks.Task<System.Net.WebSockets.WebSocket> Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature.AcceptAsync(Microsoft.AspNetCore.Http.WebSocketAcceptContext context) { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
System.Threading.Tasks.Task<System.Security.Cryptography.X509Certificates.X509Certificate2> Microsoft.AspNetCore.Http.Features.ITlsConnectionFeature.GetClientCertificateAsync(System.Threading.CancellationToken cancellationToken) { throw null; }

View File

@ -61,7 +61,7 @@ namespace Microsoft.AspNetCore.Owin
{ OwinConstants.ResponseStatusCode, new FeatureMap<IHttpResponseFeature>(feature => feature.StatusCode, () => 200, (feature, value) => feature.StatusCode = Convert.ToInt32(value)) },
{ OwinConstants.ResponseReasonPhrase, new FeatureMap<IHttpResponseFeature>(feature => feature.ReasonPhrase, (feature, value) => feature.ReasonPhrase = Convert.ToString(value)) },
{ OwinConstants.ResponseHeaders, new FeatureMap<IHttpResponseFeature>(feature => Utilities.MakeDictionaryStringArray(feature.Headers), (feature, value) => feature.Headers = Utilities.MakeHeaderDictionary((IDictionary<string, string[]>)value)) },
{ OwinConstants.ResponseBody, new FeatureMap<IHttpResponseFeature>(feature => feature.Body, () => Stream.Null, (feature, value) => feature.Body = (Stream)value) },
{ OwinConstants.ResponseBody, new FeatureMap<IHttpResponseBodyFeature>(feature => feature.Stream, () => Stream.Null, (feature, value) => context.Response.Body = (Stream)value) }, // DefaultHttpResponse.Body.Set has built in logic to handle replacing the feature.
{ OwinConstants.CommonKeys.OnSendingHeaders, new FeatureMap<IHttpResponseFeature>(
feature => new Action<Action<object>, object>((cb, state) => {
feature.OnStarting(s =>
@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Owin
{ OwinConstants.CommonKeys.RemoteIpAddress, new FeatureMap<IHttpConnectionFeature>(feature => feature.RemoteIpAddress.ToString(),
(feature, value) => feature.RemoteIpAddress = IPAddress.Parse(Convert.ToString(value))) },
{ OwinConstants.SendFiles.SendAsync, new FeatureMap<IHttpSendFileFeature>(feature => new SendFileFunc(feature.SendFileAsync)) },
{ OwinConstants.SendFiles.SendAsync, new FeatureMap<IHttpResponseBodyFeature>(feature => new SendFileFunc(feature.SendFileAsync)) },
{ OwinConstants.Security.User, new FeatureMap<IHttpAuthenticationFeature>(feature => feature.User,
()=> null, (feature, value) => feature.User = Utilities.MakeClaimsPrincipal((IPrincipal)value),

View File

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Net.WebSockets;
using System.Reflection;
@ -26,8 +27,8 @@ namespace Microsoft.AspNetCore.Owin
IFeatureCollection,
IHttpRequestFeature,
IHttpResponseFeature,
IHttpResponseBodyFeature,
IHttpConnectionFeature,
IHttpSendFileFeature,
ITlsConnectionFeature,
IHttpRequestIdentifierFeature,
IHttpRequestLifetimeFeature,
@ -36,6 +37,7 @@ namespace Microsoft.AspNetCore.Owin
IOwinEnvironmentFeature
{
public IDictionary<string, object> Environment { get; set; }
private PipeWriter _responseBodyWrapper;
private bool _headersSent;
public OwinFeatureCollection(IDictionary<string, object> environment)
@ -150,6 +152,24 @@ namespace Microsoft.AspNetCore.Owin
set { Prop(OwinConstants.ResponseBody, value); }
}
Stream IHttpResponseBodyFeature.Stream
{
get { return Prop<Stream>(OwinConstants.ResponseBody); }
}
PipeWriter IHttpResponseBodyFeature.Writer
{
get
{
if (_responseBodyWrapper == null)
{
_responseBodyWrapper = PipeWriter.Create(Prop<Stream>(OwinConstants.ResponseBody), new StreamPipeWriterOptions(leaveOpen: true));
}
return _responseBodyWrapper;
}
}
bool IHttpResponseFeature.HasStarted
{
get { return _headersSent; }
@ -202,16 +222,7 @@ namespace Microsoft.AspNetCore.Owin
set { Prop(OwinConstants.CommonKeys.ConnectionId, value); }
}
private bool SupportsSendFile
{
get
{
object obj;
return Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj) && obj != null;
}
}
Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
object obj;
if (Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj))
@ -329,11 +340,7 @@ namespace Microsoft.AspNetCore.Owin
if (key.GetTypeInfo().IsAssignableFrom(GetType().GetTypeInfo()))
{
// Check for conditional features
if (key == typeof(IHttpSendFileFeature))
{
return SupportsSendFile;
}
else if (key == typeof(ITlsConnectionFeature))
if (key == typeof(ITlsConnectionFeature))
{
return SupportsClientCerts;
}
@ -381,6 +388,7 @@ namespace Microsoft.AspNetCore.Owin
{
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestFeature), this);
yield return new KeyValuePair<Type, object>(typeof(IHttpResponseFeature), this);
yield return new KeyValuePair<Type, object>(typeof(IHttpResponseBodyFeature), this);
yield return new KeyValuePair<Type, object>(typeof(IHttpConnectionFeature), this);
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestIdentifierFeature), this);
yield return new KeyValuePair<Type, object>(typeof(IHttpRequestLifetimeFeature), this);
@ -388,10 +396,6 @@ namespace Microsoft.AspNetCore.Owin
yield return new KeyValuePair<Type, object>(typeof(IOwinEnvironmentFeature), this);
// Check for conditional features
if (SupportsSendFile)
{
yield return new KeyValuePair<Type, object>(typeof(IHttpSendFileFeature), this);
}
if (SupportsClientCerts)
{
yield return new KeyValuePair<Type, object>(typeof(ITlsConnectionFeature), this);
@ -402,6 +406,31 @@ namespace Microsoft.AspNetCore.Owin
}
}
void IHttpResponseBodyFeature.DisableBuffering()
{
}
async Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken)
{
if (_responseBodyWrapper != null)
{
await _responseBodyWrapper.FlushAsync(cancellationToken);
}
// The pipe may or may not have flushed the stream. Make sure the stream gets flushed to trigger response start.
await Prop<Stream>(OwinConstants.ResponseBody).FlushAsync(cancellationToken);
}
Task IHttpResponseBodyFeature.CompleteAsync()
{
if (_responseBodyWrapper != null)
{
return _responseBodyWrapper.FlushAsync().AsTask();
}
return Task.CompletedTask;
}
public void Dispose()
{
}

View File

@ -0,0 +1,87 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Http
{
// FYI: In most cases the source will be a FileStream and the destination will be to the network.
internal static class StreamCopyOperationInternal
{
private const int DefaultBufferSize = 4096;
/// <summary>Asynchronously reads the given number of bytes from the source stream and writes them to another stream.</summary>
/// <returns>A task that represents the asynchronous copy operation.</returns>
/// <param name="source">The stream from which the contents will be copied.</param>
/// <param name="destination">The stream to which the contents of the current stream will be copied.</param>
/// <param name="count">The count of bytes to be copied.</param>
/// <param name="cancel">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel)
{
return CopyToAsync(source, destination, count, DefaultBufferSize, cancel);
}
/// <summary>Asynchronously reads the given number of bytes from the source stream and writes them to another stream, using a specified buffer size.</summary>
/// <returns>A task that represents the asynchronous copy operation.</returns>
/// <param name="source">The stream from which the contents will be copied.</param>
/// <param name="destination">The stream to which the contents of the current stream will be copied.</param>
/// <param name="count">The count of bytes to be copied.</param>
/// <param name="bufferSize">The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096.</param>
/// <param name="cancel">The token to monitor for cancellation requests. The default value is <see cref="P:System.Threading.CancellationToken.None" />.</param>
public static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel)
{
long? bytesRemaining = count;
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
Debug.Assert(source != null);
Debug.Assert(destination != null);
Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0);
Debug.Assert(buffer != null);
while (true)
{
// The natural end of the range.
if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0)
{
return;
}
cancel.ThrowIfCancellationRequested();
int readLength = buffer.Length;
if (bytesRemaining.HasValue)
{
readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength);
}
int read = await source.ReadAsync(buffer, 0, readLength, cancel);
if (bytesRemaining.HasValue)
{
bytesRemaining -= read;
}
// End of the source stream.
if (read == 0)
{
return;
}
cancel.ThrowIfCancellationRequested();
await destination.WriteAsync(buffer, 0, read, cancel);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
}

View File

@ -116,8 +116,6 @@ namespace Microsoft.AspNetCore.Diagnostics
// add response buffering
app.Use(async (httpContext, next) =>
{
httpContext.Features.Set<IHttpResponseStartFeature>(null);
var response = httpContext.Response;
var originalResponseBody = response.Body;
var bufferingStream = new MemoryStream();

View File

@ -53,8 +53,6 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal ResponseCachingStream ResponseCachingStream { get; set; }
internal IHttpSendFileFeature OriginalSendFileFeature { get; set; }
internal IHeaderDictionary CachedResponseHeaders { get; set; }
internal DateTimeOffset? ResponseDate

View File

@ -433,13 +433,6 @@ namespace Microsoft.AspNetCore.ResponseCaching
() => StartResponseAsync(context));
context.HttpContext.Response.Body = context.ResponseCachingStream;
// Shim IHttpSendFileFeature
context.OriginalSendFileFeature = context.HttpContext.Features.Get<IHttpSendFileFeature>();
if (context.OriginalSendFileFeature != null)
{
context.HttpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCachingStream));
}
// Add IResponseCachingFeature
AddResponseCachingFeature(context.HttpContext);
}
@ -452,9 +445,6 @@ namespace Microsoft.AspNetCore.ResponseCaching
// Unshim response stream
context.HttpContext.Response.Body = context.OriginalResponseStream;
// Unshim IHttpSendFileFeature
context.HttpContext.Features.Set(context.OriginalSendFileFeature);
// Remove IResponseCachingFeature
RemoveResponseCachingFeature(context.HttpContext);
}

View File

@ -1,28 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.ResponseCaching
{
internal class SendFileFeatureWrapper : IHttpSendFileFeature
{
private readonly IHttpSendFileFeature _originalSendFileFeature;
private readonly ResponseCachingStream _responseCachingStream;
public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCachingStream responseCachingStream)
{
_originalSendFileFeature = originalSendFileFeature;
_responseCachingStream = responseCachingStream;
}
// Flush and disable the buffer if anyone tries to call the SendFile feature.
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
_responseCachingStream.DisableBuffering();
return _originalSendFileFeature.SendFileAsync(path, offset, count, cancellation);
}
}
}

View File

@ -4,6 +4,13 @@
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Include="TestDocument.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.ResponseCaching" />
<Reference Include="Microsoft.AspNetCore.TestHost" />

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@ -475,58 +476,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
}
[Fact]
public async Task ServesCachedContent_IfIHttpSendFileFeature_NotUsed()
{
var builders = TestUtils.CreateBuildersWithResponseCaching(app =>
{
app.Use(async (context, next) =>
{
context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
await next.Invoke();
});
});
foreach (var builder in builders)
{
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var initialResponse = await client.GetAsync("");
var subsequentResponse = await client.GetAsync("");
await AssertCachedResponseAsync(initialResponse, subsequentResponse);
}
}
}
[Fact]
public async Task ServesFreshContent_IfIHttpSendFileFeature_Used()
{
var builders = TestUtils.CreateBuildersWithResponseCaching(
app =>
{
app.Use(async (context, next) =>
{
context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
await next.Invoke();
});
},
contextAction: async context => await context.Features.Get<IHttpSendFileFeature>().SendFileAsync("dummy", 0, 0, CancellationToken.None));
foreach (var builder in builders)
{
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var initialResponse = await client.GetAsync("");
var subsequentResponse = await client.GetAsync("");
await AssertFreshResponseAsync(initialResponse, subsequentResponse);
}
}
}
[Fact]
public async Task ServesCachedContent_IfSubsequentRequestContainsNoStore()
{

View File

@ -0,0 +1 @@
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ

View File

@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Net.Http;
using System.Text;
@ -75,6 +77,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
}
internal static async Task TestRequestDelegateSendFileAsync(HttpContext context)
{
var path = Path.Combine(AppContext.BaseDirectory, "TestDocument.txt");
var uniqueId = Guid.NewGuid().ToString();
if (TestRequestDelegate(context, uniqueId))
{
await context.Response.SendFileAsync(path, 0, null);
await context.Response.WriteAsync(uniqueId);
}
}
internal static Task TestRequestDelegateWrite(HttpContext context)
{
var uniqueId = Guid.NewGuid().ToString();
@ -117,6 +130,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
contextAction?.Invoke(context);
return TestRequestDelegateWriteAsync(context);
},
context =>
{
contextAction?.Invoke(context);
return TestRequestDelegateSendFileAsync(context);
},
});
}
@ -296,14 +314,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
internal LogLevel LogLevel { get; }
}
internal class DummySendFileFeature : IHttpSendFileFeature
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return Task.CompletedTask;
}
}
internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider
{
public bool AllowCacheLookupValue { get; set; } = false;

View File

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<Compile Include="Microsoft.AspNetCore.ResponseCompression.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
@ -53,7 +53,7 @@ namespace ResponseCompressionSample
{
context.Response.ContentType = "text/plain";
// Disables compression on net451 because that GZipStream does not implement Flush.
context.Features.Get<IHttpBufferingFeature>()?.DisableResponseBuffering();
context.Features.Get<IHttpResponseBodyFeature>().DisableBuffering();
for (int i = 0; i < 100; i++)
{

View File

@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.Http.Extensions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
@ -15,35 +16,48 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <summary>
/// Stream wrapper that create specific compression stream only if necessary.
/// </summary>
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature, IHttpResponseStartFeature, IHttpsCompressionFeature
internal class ResponseCompressionBody : Stream, IHttpResponseBodyFeature, IHttpsCompressionFeature
{
private readonly HttpContext _context;
private readonly Stream _bodyOriginalStream;
private readonly IResponseCompressionProvider _provider;
private readonly IHttpBufferingFeature _innerBufferFeature;
private readonly IHttpSendFileFeature _innerSendFileFeature;
private readonly IHttpResponseStartFeature _innerStartFeature;
private readonly IHttpResponseBodyFeature _innerBodyFeature;
private ICompressionProvider _compressionProvider = null;
private bool _compressionChecked = false;
private Stream _compressionStream = null;
private Stream _innerStream = null;
private PipeWriter _pipeAdapter = null;
private bool _providerCreated = false;
private bool _autoFlush = false;
private bool _complete = false;
internal BodyWrapperStream(HttpContext context, Stream bodyOriginalStream, IResponseCompressionProvider provider,
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature, IHttpResponseStartFeature innerStartFeature)
internal ResponseCompressionBody(HttpContext context, IResponseCompressionProvider provider,
IHttpResponseBodyFeature innerBodyFeature)
{
_context = context;
_bodyOriginalStream = bodyOriginalStream;
_provider = provider;
_innerBufferFeature = innerBufferFeature;
_innerSendFileFeature = innerSendFileFeature;
_innerStartFeature = innerStartFeature;
_innerBodyFeature = innerBodyFeature;
_innerStream = innerBodyFeature.Stream;
}
internal ValueTask FinishCompressionAsync()
internal async Task FinishCompressionAsync()
{
return _compressionStream?.DisposeAsync() ?? new ValueTask();
if (_complete)
{
return;
}
_complete = true;
if (_pipeAdapter != null)
{
await _pipeAdapter.CompleteAsync();
}
if (_compressionStream != null)
{
await _compressionStream.DisposeAsync();
}
}
HttpsCompressionMode IHttpsCompressionFeature.Mode { get; set; } = HttpsCompressionMode.Default;
@ -52,7 +66,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
public override bool CanSeek => false;
public override bool CanWrite => _bodyOriginalStream.CanWrite;
public override bool CanWrite => _innerStream.CanWrite;
public override long Length
{
@ -65,6 +79,21 @@ namespace Microsoft.AspNetCore.ResponseCompression
set { throw new NotSupportedException(); }
}
public Stream Stream => this;
public PipeWriter Writer
{
get
{
if (_pipeAdapter == null)
{
_pipeAdapter = PipeWriter.Create(Stream, new StreamPipeWriterOptions(leaveOpen: true));
}
return _pipeAdapter;
}
}
public override void Flush()
{
if (!_compressionChecked)
@ -72,7 +101,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
OnWrite();
// Flush the original stream to send the headers. Flushing the compression stream won't
// flush the original stream if no data has been written yet.
_bodyOriginalStream.Flush();
_innerStream.Flush();
return;
}
@ -82,7 +111,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
}
else
{
_bodyOriginalStream.Flush();
_innerStream.Flush();
}
}
@ -93,7 +122,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
OnWrite();
// Flush the original stream to send the headers. Flushing the compression stream won't
// flush the original stream if no data has been written yet.
return _bodyOriginalStream.FlushAsync(cancellationToken);
return _innerStream.FlushAsync(cancellationToken);
}
if (_compressionStream != null)
@ -101,7 +130,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
return _compressionStream.FlushAsync(cancellationToken);
}
return _bodyOriginalStream.FlushAsync(cancellationToken);
return _innerStream.FlushAsync(cancellationToken);
}
public override int Read(byte[] buffer, int offset, int count)
@ -133,7 +162,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
}
else
{
_bodyOriginalStream.Write(buffer, offset, count);
_innerStream.Write(buffer, offset, count);
}
}
@ -198,7 +227,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
}
else
{
await _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
}
}
@ -234,7 +263,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
_context.Response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
_context.Response.Headers.Remove(HeaderNames.ContentLength);
_compressionStream = compressionProvider.CreateStream(_bodyOriginalStream);
_compressionStream = compressionProvider.CreateStream(_innerStream);
}
}
}
@ -251,14 +280,8 @@ namespace Microsoft.AspNetCore.ResponseCompression
return _compressionProvider;
}
public void DisableRequestBuffering()
{
// Unrelated
_innerBufferFeature?.DisableRequestBuffering();
}
// For this to be effective it needs to be called before the first write.
public void DisableResponseBuffering()
public void DisableBuffering()
{
if (ResolveCompressionProvider()?.SupportsFlush == false)
{
@ -270,69 +293,36 @@ namespace Microsoft.AspNetCore.ResponseCompression
{
_autoFlush = true;
}
_innerBufferFeature?.DisableResponseBuffering();
_innerBodyFeature.DisableBuffering();
}
// The IHttpSendFileFeature feature will only be registered if _innerSendFileFeature exists.
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
OnWrite();
if (_compressionStream != null)
{
return InnerSendFileAsync(path, offset, count, cancellation);
return SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellation);
}
return _innerSendFileFeature.SendFileAsync(path, offset, count, cancellation);
}
private async Task InnerSendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
cancellation.ThrowIfCancellationRequested();
var fileInfo = new FileInfo(path);
if (offset < 0 || offset > fileInfo.Length)
{
throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty);
}
if (count.HasValue &&
(count.Value < 0 || count.Value > fileInfo.Length - offset))
{
throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty);
}
int bufferSize = 1024 * 16;
var fileStream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite,
bufferSize: bufferSize,
options: FileOptions.Asynchronous | FileOptions.SequentialScan);
using (fileStream)
{
fileStream.Seek(offset, SeekOrigin.Begin);
await StreamCopyOperation.CopyToAsync(fileStream, _compressionStream, count, cancellation);
if (_autoFlush)
{
await _compressionStream.FlushAsync(cancellation);
}
}
return _innerBodyFeature.SendFileAsync(path, offset, count, cancellation);
}
public Task StartAsync(CancellationToken token = default)
{
OnWrite();
return _innerBodyFeature.StartAsync(token);
}
if (_innerStartFeature != null)
public async Task CompleteAsync()
{
if (_complete)
{
return _innerStartFeature.StartAsync(token);
return;
}
return Task.CompletedTask;
await FinishCompressionAsync(); // Sets _complete
await _innerBodyFeature.CompleteAsync();
}
}
}

View File

@ -51,46 +51,22 @@ namespace Microsoft.AspNetCore.ResponseCompression
return;
}
var bodyStream = context.Response.Body;
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
var originalStartFeature = context.Features.Get<IHttpResponseStartFeature>();
var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>();
var originalCompressionFeature = context.Features.Get<IHttpsCompressionFeature>();
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
originalBufferFeature, originalSendFileFeature, originalStartFeature);
context.Response.Body = bodyWrapperStream;
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
context.Features.Set<IHttpsCompressionFeature>(bodyWrapperStream);
if (originalSendFileFeature != null)
{
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
}
if (originalStartFeature != null)
{
context.Features.Set<IHttpResponseStartFeature>(bodyWrapperStream);
}
var compressionBody = new ResponseCompressionBody(context, _provider, originalBodyFeature);
context.Features.Set<IHttpResponseBodyFeature>(compressionBody);
context.Features.Set<IHttpsCompressionFeature>(compressionBody);
try
{
await _next(context);
await bodyWrapperStream.FinishCompressionAsync();
await compressionBody.FinishCompressionAsync();
}
finally
{
context.Response.Body = bodyStream;
context.Features.Set(originalBufferFeature);
context.Features.Set(originalBodyFeature);
context.Features.Set(originalCompressionFeature);
if (originalSendFileFeature != null)
{
context.Features.Set(originalSendFileFeature);
}
if (originalStartFeature != null)
{
context.Features.Set(originalStartFeature);
}
}
}
}

View File

@ -1,19 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
public class BodyWrapperStreamTests
public class ResponseCompressionBodyTest
{
[Theory]
[InlineData(null, "Accept-Encoding")]
@ -26,7 +23,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
var httpContext = new DefaultHttpContext();
httpContext.Response.Headers[HeaderNames.Vary] = providedVaryHeader;
var stream = new BodyWrapperStream(httpContext, new MemoryStream(), new MockResponseCompressionProvider(flushable: true), null, null, null);
var stream = new ResponseCompressionBody(httpContext, new MockResponseCompressionProvider(flushable: true), new StreamResponseBodyFeature(new MemoryStream()));
stream.Write(new byte[] { }, 0, 0);
@ -41,22 +38,14 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
var buffer = new byte[] { 1 };
byte[] written = null;
var mock = new Mock<Stream>();
mock.SetupGet(s => s.CanWrite).Returns(true);
mock.Setup(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()))
.Callback<byte[], int, int>((b, o, c) =>
{
written = new ArraySegment<byte>(b, 0, c).ToArray();
});
var memoryStream = new MemoryStream();
var stream = new ResponseCompressionBody(new DefaultHttpContext(), new MockResponseCompressionProvider(flushable), new StreamResponseBodyFeature(memoryStream));
var stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(flushable), null, null, null);
stream.DisableResponseBuffering();
stream.DisableBuffering();
stream.Write(buffer, 0, buffer.Length);
Assert.Equal(buffer, written);
Assert.Equal(buffer, memoryStream.ToArray());
}
[Theory]
@ -67,9 +56,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
var buffer = new byte[] { 1 };
var memoryStream = new MemoryStream();
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null, null);
var stream = new ResponseCompressionBody(new DefaultHttpContext(), new MockResponseCompressionProvider(flushable), new StreamResponseBodyFeature(memoryStream));
stream.DisableResponseBuffering();
stream.DisableBuffering();
await stream.WriteAsync(buffer, 0, buffer.Length);
Assert.Equal(buffer, memoryStream.ToArray());
@ -80,9 +69,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
var memoryStream = new MemoryStream();
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(true), null, null, null);
var stream = new ResponseCompressionBody(new DefaultHttpContext(), new MockResponseCompressionProvider(true), new StreamResponseBodyFeature(memoryStream));
stream.DisableResponseBuffering();
stream.DisableBuffering();
var path = "testfile1kb.txt";
await stream.SendFileAsync(path, 0, null, CancellationToken.None);
@ -99,9 +88,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
var memoryStream = new MemoryStream();
var stream = new BodyWrapperStream(new DefaultHttpContext(), memoryStream, new MockResponseCompressionProvider(flushable), null, null, null);
var stream = new ResponseCompressionBody(new DefaultHttpContext(), new MockResponseCompressionProvider(flushable), new StreamResponseBodyFeature(memoryStream));
stream.DisableResponseBuffering();
stream.DisableBuffering();
stream.BeginWrite(buffer, 0, buffer.Length, (o) => {}, null);
Assert.Equal(buffer, memoryStream.ToArray());

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Pipelines;
using System.Linq;
using System.Net.Http;
using System.Threading;
@ -772,7 +773,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
context.Features.Get<IHttpBufferingFeature>()?.DisableResponseBuffering();
context.Features.Get<IHttpResponseBodyFeature>().DisableBuffering();
var feature = context.Features.Get<IHttpBodyControlFeature>();
if (feature != null)
@ -835,7 +836,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
context.Features.Get<IHttpBufferingFeature>()?.DisableResponseBuffering();
context.Features.Get<IHttpResponseBodyFeature>().DisableBuffering();
foreach (var signal in responseReceived)
{
@ -867,38 +868,6 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
}
}
[Fact]
public async Task SendFileAsync_OnlySetIfFeatureAlreadyExists()
{
var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddResponseCompression();
})
.Configure(app =>
{
app.UseResponseCompression();
app.Run(context =>
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
context.Response.ContentLength = 1024;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
Assert.Null(sendFile);
return Task.FromResult(0);
});
});
var server = new TestServer(builder);
var client = server.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "");
request.Headers.AcceptEncoding.ParseAdd("gzip");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
}
[Fact]
public async Task SendFileAsync_DifferentContentType_NotBypassed()
{
@ -913,8 +882,8 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
app.Use((context, next) =>
{
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
fakeSendFile = new FakeSendFileFeature(context.Features.Get<IHttpResponseBodyFeature>());
context.Features.Set<IHttpResponseBodyFeature>(fakeSendFile);
return next();
});
app.UseResponseCompression();
@ -923,7 +892,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = "custom/type";
context.Response.ContentLength = 1024;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
var sendFile = context.Features.Get<IHttpResponseBodyFeature>();
Assert.NotNull(sendFile);
return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
});
@ -939,7 +908,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
CheckResponseNotCompressed(response, expectedBodyLength: 1024, sendVaryHeader: false);
Assert.True(fakeSendFile.Invoked);
Assert.True(fakeSendFile.SendFileInvoked);
}
[Fact]
@ -956,8 +925,8 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
app.Use((context, next) =>
{
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
fakeSendFile = new FakeSendFileFeature(context.Features.Get<IHttpResponseBodyFeature>());
context.Features.Set<IHttpResponseBodyFeature>(fakeSendFile);
return next();
});
app.UseResponseCompression();
@ -966,7 +935,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
context.Response.ContentLength = 1024;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
var sendFile = context.Features.Get<IHttpResponseBodyFeature>();
Assert.NotNull(sendFile);
return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
});
@ -982,7 +951,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
CheckResponseCompressed(response, expectedBodyLength: 34, expectedEncoding: "gzip");
Assert.False(fakeSendFile.Invoked);
Assert.False(fakeSendFile.SendFileInvoked);
}
[Fact]
@ -999,8 +968,8 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
app.Use((context, next) =>
{
fakeSendFile = new FakeSendFileFeature(context.Response.Body);
context.Features.Set<IHttpSendFileFeature>(fakeSendFile);
fakeSendFile = new FakeSendFileFeature(context.Features.Get<IHttpResponseBodyFeature>());
context.Features.Set<IHttpResponseBodyFeature>(fakeSendFile);
return next();
});
app.UseResponseCompression();
@ -1008,11 +977,10 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = TextPlain;
var sendFile = context.Features.Get<IHttpSendFileFeature>();
Assert.NotNull(sendFile);
var feature = context.Features.Get<IHttpResponseBodyFeature>();
await context.Response.WriteAsync(new string('a', 100));
await sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
await feature.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
});
});
@ -1026,7 +994,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
CheckResponseCompressed(response, expectedBodyLength: 46, expectedEncoding: "gzip");
Assert.False(fakeSendFile.Invoked);
Assert.False(fakeSendFile.SendFileInvoked);
}
[Theory]
@ -1105,7 +1073,6 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = responseType;
Assert.Null(context.Features.Get<IHttpSendFileFeature>());
addResponseAction?.Invoke(context.Response);
return context.Response.WriteAsync(new string('a', uncompressedBodyLength));
});
@ -1178,31 +1145,33 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
AssertLog(logMessages.Skip(2).First(), LogLevel.Debug, $"The response will be compressed with '{provider}'.");
}
private class FakeSendFileFeature : IHttpSendFileFeature
private class FakeSendFileFeature : IHttpResponseBodyFeature
{
private readonly Stream _innerBody;
public FakeSendFileFeature(Stream innerBody)
public FakeSendFileFeature(IHttpResponseBodyFeature innerFeature)
{
_innerBody = innerBody;
InnerFeature = innerFeature;
}
public bool Invoked { get; set; }
public IHttpResponseBodyFeature InnerFeature { get; }
public async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
public bool SendFileInvoked { get; set; }
public Stream Stream => InnerFeature.Stream;
public PipeWriter Writer => InnerFeature.Writer;
public Task CompleteAsync() => InnerFeature.CompleteAsync();
public void DisableBuffering() => InnerFeature.DisableBuffering();
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
// This implementation should only be delegated to if compression is disabled.
Invoked = true;
using (var file = new FileStream(path, FileMode.Open))
{
file.Seek(offset, SeekOrigin.Begin);
if (count.HasValue)
{
throw new NotImplementedException("Not implemented for testing");
}
await file.CopyToAsync(_innerBody, 81920, cancellation);
}
SendFileInvoked = true;
return InnerFeature.SendFileAsync(path, offset, count, cancellation);
}
public Task StartAsync(CancellationToken token = default) => InnerFeature.StartAsync(token);
}
private readonly struct EncodingTestData

View File

@ -346,7 +346,7 @@ namespace Microsoft.AspNetCore.StaticFiles
SetCompressionMode();
ApplyResponseHeaders(Constants.Status200Ok);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpSendFileFeature>();
var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
// We don't need to directly cancel this, if the client disconnects it will fail silently.
@ -392,7 +392,7 @@ namespace Microsoft.AspNetCore.StaticFiles
ApplyResponseHeaders(Constants.Status206PartialContent);
string physicalPath = _fileInfo.PhysicalPath;
var sendFile = _context.Features.Get<IHttpSendFileFeature>();
var sendFile = _context.Features.Get<IHttpResponseBodyFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
{
_logger.SendingFileRange(_response.Headers[HeaderNames.ContentRange], physicalPath);

View File

@ -66,9 +66,10 @@ namespace Microsoft.AspNetCore.StaticFiles
[Fact]
public async Task ReturnsNotFoundIfSendFileThrows()
{
var mockSendFile = new Mock<IHttpSendFileFeature>();
var mockSendFile = new Mock<IHttpResponseBodyFeature>();
mockSendFile.Setup(m => m.SendFileAsync(It.IsAny<string>(), It.IsAny<long>(), It.IsAny<long?>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new FileNotFoundException());
mockSendFile.Setup(m => m.Stream).Returns(Stream.Null);
var builder = new WebHostBuilder()
.Configure(app =>
{

View File

@ -2428,6 +2428,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
public PhysicalFileResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) : base (default(Microsoft.Extensions.Logging.ILogger)) { }
public virtual System.Threading.Tasks.Task ExecuteAsync(Microsoft.AspNetCore.Mvc.ActionContext context, Microsoft.AspNetCore.Mvc.PhysicalFileResult result) { throw null; }
protected virtual Microsoft.AspNetCore.Mvc.Infrastructure.PhysicalFileResultExecutor.FileMetadata GetFileInfo(string path) { throw null; }
[System.ObsoleteAttribute("This API is no longer called.")]
protected virtual System.IO.Stream GetFileStream(string path) { throw null; }
protected virtual System.Threading.Tasks.Task WriteFileAsync(Microsoft.AspNetCore.Mvc.ActionContext context, Microsoft.AspNetCore.Mvc.PhysicalFileResult result, Microsoft.Net.Http.Headers.RangeItemHeaderValue range, long rangeLength) { throw null; }
protected partial class FileMetadata
@ -2462,6 +2463,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
{
public VirtualFileResultExecutor(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment) : base (default(Microsoft.Extensions.Logging.ILogger)) { }
public virtual System.Threading.Tasks.Task ExecuteAsync(Microsoft.AspNetCore.Mvc.ActionContext context, Microsoft.AspNetCore.Mvc.VirtualFileResult result) { throw null; }
[System.ObsoleteAttribute("This API is no longer called.")]
protected virtual System.IO.Stream GetFileStream(Microsoft.Extensions.FileProviders.IFileInfo fileInfo) { throw null; }
protected virtual System.Threading.Tasks.Task WriteFileAsync(Microsoft.AspNetCore.Mvc.ActionContext context, Microsoft.AspNetCore.Mvc.VirtualFileResult result, Microsoft.Extensions.FileProviders.IFileInfo fileInfo, Microsoft.Net.Http.Headers.RangeItemHeaderValue range, long rangeLength) { throw null; }
}

View File

@ -5,6 +5,7 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Logging;
@ -86,28 +87,19 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Logger.WritingRangeToBody();
}
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
if (sendFile != null)
if (range != null)
{
if (range != null)
{
return sendFile.SendFileAsync(
result.FileName,
offset: range.From ?? 0L,
count: rangeLength,
cancellation: default(CancellationToken));
}
return sendFile.SendFileAsync(
result.FileName,
offset: 0,
count: null,
cancellation: default(CancellationToken));
return response.SendFileAsync(result.FileName,
offset: range.From ?? 0L,
count: rangeLength);
}
return WriteFileAsync(context.HttpContext, GetFileStream(result.FileName), range, rangeLength);
return response.SendFileAsync(result.FileName,
offset: 0,
count: null);
}
[Obsolete("This API is no longer called.")]
protected virtual Stream GetFileStream(string path)
{
if (path == null)

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.FileProviders;
@ -86,33 +87,22 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
}
var response = context.HttpContext.Response;
var physicalPath = fileInfo.PhysicalPath;
if (range != null)
{
Logger.WritingRangeToBody();
}
var sendFile = response.HttpContext.Features.Get<IHttpSendFileFeature>();
if (sendFile != null && !string.IsNullOrEmpty(physicalPath))
if (range != null)
{
if (range != null)
{
return sendFile.SendFileAsync(
physicalPath,
offset: range.From ?? 0L,
count: rangeLength,
cancellation: default(CancellationToken));
}
return sendFile.SendFileAsync(
physicalPath,
offset: 0,
count: null,
cancellation: default(CancellationToken));
return response.SendFileAsync(fileInfo,
offset: range.From ?? 0L,
count: rangeLength);
}
return WriteFileAsync(context.HttpContext, GetFileStream(fileInfo), range, rangeLength);
return response.SendFileAsync(fileInfo,
offset: 0,
count: null);
}
private IFileInfo GetFileInformation(VirtualFileResult result)
@ -144,6 +134,7 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
return result.FileProvider;
}
[Obsolete("This API is no longer called.")]
protected virtual Stream GetFileStream(IFileInfo fileInfo)
{
return fileInfo.CreateReadStream();

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -62,31 +63,31 @@ namespace Microsoft.AspNetCore.Mvc
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
result.EnableRangeProcessing = true;
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(start, end);
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 34 - end;
end = start + contentLength - 1;
var startResult = start ?? 34 - end;
var endResult = startResult + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 34);
var contentRange = new ContentRangeHeaderValue(startResult.Value, endResult.Value, 34);
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(contentLength, httpResponse.ContentLength);
Assert.Equal(expectedString, body);
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
Assert.Equal(startResult, sendFile.Offset);
Assert.Equal((long?)contentLength, sendFile.Length);
}
[Fact]
@ -97,13 +98,14 @@ namespace Microsoft.AspNetCore.Mvc
var result = new TestPhysicalFileResult(path, "text/plain");
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
result.EnableRangeProcessing = true;
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -111,9 +113,6 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
var contentRange = new ContentRangeHeaderValue(0, 3, 34);
@ -121,7 +120,9 @@ namespace Microsoft.AspNetCore.Mvc
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(4, httpResponse.ContentLength);
Assert.Equal("File", body);
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
Assert.Equal(0, sendFile.Offset);
Assert.Equal(4, sendFile.Length);
}
[Fact]
@ -131,13 +132,14 @@ namespace Microsoft.AspNetCore.Mvc
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -145,12 +147,11 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal("FilePathResultTestFile contents<74>", body);
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
Assert.Equal(0, sendFile.Offset);
Assert.Null(sendFile.Length);
}
[Fact]
@ -161,13 +162,14 @@ namespace Microsoft.AspNetCore.Mvc
var result = new TestPhysicalFileResult(path, "text/plain");
result.EnableRangeProcessing = true;
var entityTag = result.EntityTag = new EntityTagHeaderValue("\"Etag\"");
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -175,12 +177,11 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal("FilePathResultTestFile contents<74>", body);
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
Assert.Equal(0, sendFile.Offset);
Assert.Null(sendFile.Length);
}
[Theory]
@ -192,12 +193,13 @@ namespace Microsoft.AspNetCore.Mvc
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -205,13 +207,12 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal("FilePathResultTestFile contents<74>", body);
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
Assert.Equal(0, sendFile.Offset);
Assert.Null(sendFile.Length);
}
[Theory]
@ -309,33 +310,13 @@ namespace Microsoft.AspNetCore.Mvc
Assert.Empty(body);
}
[Fact]
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
httpContext.Response.Body.Position = 0;
// Assert
Assert.NotNull(httpContext.Response.Body);
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
Assert.Equal("FilePathResultTestFile contents<74>", contents);
}
[Fact]
public async Task ExecuteResultAsync_CallsSendFileAsync_IfIHttpSendFilePresent()
{
// Arrange
var path = Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var sendFileMock = new Mock<IHttpSendFileFeature>();
var sendFileMock = new Mock<IHttpResponseBodyFeature>();
sendFileMock
.Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None))
.Returns(Task.FromResult<int>(0));
@ -364,7 +345,7 @@ namespace Microsoft.AspNetCore.Mvc
result.EnableRangeProcessing = true;
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpSendFileFeature>(sendFile);
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var requestHeaders = httpContext.Request.GetTypedHeaders();
requestHeaders.Range = new RangeHeaderValue(start, end);
@ -397,22 +378,21 @@ namespace Microsoft.AspNetCore.Mvc
// Arrange
var expectedContentType = "text/foo; charset=us-ascii";
var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile_ASCII.txt"));
var result = new TestPhysicalFileResult(path, expectedContentType)
{
IsAscii = true
};
var result = new TestPhysicalFileResult(path, expectedContentType);
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
var memoryStream = new MemoryStream();
httpContext.Response.Body = memoryStream;
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
// Assert
var contents = Encoding.ASCII.GetString(memoryStream.ToArray());
Assert.Equal("FilePathResultTestFile contents ASCII encoded", contents);
Assert.Equal(expectedContentType, httpContext.Response.ContentType);
Assert.Equal(Path.GetFullPath(Path.Combine("TestFiles", "FilePathResultTestFile_ASCII.txt")), sendFile.Name);
Assert.Equal(0, sendFile.Offset);
Assert.Null(sendFile.Length);
Assert.Equal(CancellationToken.None, sendFile.Token);
}
[Fact]
@ -422,19 +402,20 @@ namespace Microsoft.AspNetCore.Mvc
var path = Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt"));
var result = new TestPhysicalFileResult(path, "text/plain");
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
httpContext.Response.Body.Position = 0;
// Assert
Assert.NotNull(httpContext.Response.Body);
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
Assert.Equal("FilePathResultTestFile contents<74>", contents);
Assert.Equal(Path.GetFullPath(Path.Combine(".", "TestFiles", "FilePathResultTestFile.txt")), sendFile.Name);
Assert.Equal(0, sendFile.Offset);
Assert.Null(sendFile.Length);
Assert.Equal(CancellationToken.None, sendFile.Token);
}
[Theory]
@ -506,11 +487,8 @@ namespace Microsoft.AspNetCore.Mvc
public override Task ExecuteResultAsync(ActionContext context)
{
var executor = context.HttpContext.RequestServices.GetRequiredService<TestPhysicalFileResultExecutor>();
executor.IsAscii = IsAscii;
return executor.ExecuteAsync(context, this);
}
public bool IsAscii { get; set; } = false;
}
private class TestPhysicalFileResultExecutor : PhysicalFileResultExecutor
@ -520,20 +498,6 @@ namespace Microsoft.AspNetCore.Mvc
{
}
public bool IsAscii { get; set; } = false;
protected override Stream GetFileStream(string path)
{
if (IsAscii)
{
return new MemoryStream(Encoding.ASCII.GetBytes("FilePathResultTestFile contents ASCII encoded"));
}
else
{
return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents<74>"));
}
}
protected override FileMetadata GetFileInfo(string path)
{
var lastModified = DateTimeOffset.MinValue.AddDays(1);
@ -546,21 +510,39 @@ namespace Microsoft.AspNetCore.Mvc
}
}
private class TestSendFileFeature : IHttpSendFileFeature
private class TestSendFileFeature : IHttpResponseBodyFeature
{
public string Name { get; set; }
public long Offset { get; set; }
public long? Length { get; set; }
public CancellationToken Token { get; set; }
public Stream Stream => throw new NotImplementedException();
public PipeWriter Writer => throw new NotImplementedException();
public Task CompleteAsync()
{
throw new NotImplementedException();
}
public void DisableBuffering()
{
throw new NotImplementedException();
}
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
Name = path;
Offset = offset;
Length = length;
Token = cancellation;
return Task.CompletedTask;
}
return Task.FromResult(0);
public Task StartAsync(CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.IO.Pipelines;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -69,8 +70,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -87,19 +89,18 @@ namespace Microsoft.AspNetCore.Mvc
await result.ExecuteResultAsync(actionContext);
// Assert
start = start ?? 33 - end;
end = start + contentLength - 1;
var startResult = start ?? 33 - end;
var endResult = startResult + contentLength - 1;
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
var contentRange = new ContentRangeHeaderValue(start.Value, end.Value, 33);
var contentRange = new ContentRangeHeaderValue(startResult.Value, endResult.Value, 33);
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal(contentLength, httpResponse.ContentLength);
Assert.Equal(expectedString, body);
Assert.Equal(path, sendFileFeature.Name);
Assert.Equal(startResult, sendFileFeature.Offset);
Assert.Equal((long?)contentLength, sendFileFeature.Length);
}
[Fact]
@ -114,8 +115,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -128,7 +130,6 @@ namespace Microsoft.AspNetCore.Mvc
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -136,16 +137,15 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status206PartialContent, httpResponse.StatusCode);
Assert.Equal("bytes", httpResponse.Headers[HeaderNames.AcceptRanges]);
var contentRange = new ContentRangeHeaderValue(0, 3, 33);
Assert.Equal(contentRange.ToString(), httpResponse.Headers[HeaderNames.ContentRange]);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal(4, httpResponse.ContentLength);
Assert.Equal("File", body);
Assert.Equal(path, sendFileFeature.Name);
Assert.Equal(0, sendFileFeature.Offset);
Assert.Equal(4, sendFileFeature.Length);
}
[Fact]
@ -159,8 +159,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -173,7 +174,6 @@ namespace Microsoft.AspNetCore.Mvc
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"Etag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -181,12 +181,11 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("FilePathResultTestFile contents¡", body);
Assert.Equal(path, sendFileFeature.Name);
Assert.Equal(0, sendFileFeature.Offset);
Assert.Null(sendFileFeature.Length);
}
[Fact]
@ -201,8 +200,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -215,7 +215,6 @@ namespace Microsoft.AspNetCore.Mvc
requestHeaders.Range = new RangeHeaderValue(0, 3);
requestHeaders.IfRange = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"NotEtag\""));
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -223,12 +222,11 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Equal(entityTag.ToString(), httpResponse.Headers[HeaderNames.ETag]);
Assert.Equal("FilePathResultTestFile contents¡", body);
Assert.Equal(path, sendFileFeature.Name);
Assert.Equal(0, sendFileFeature.Offset);
Assert.Null(sendFileFeature.Length);
}
[Theory]
@ -246,8 +244,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -258,7 +257,6 @@ namespace Microsoft.AspNetCore.Mvc
httpContext.Request.Headers[HeaderNames.Range] = rangeString;
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -266,13 +264,12 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status200OK, httpResponse.StatusCode);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Equal("FilePathResultTestFile contents¡", body);
Assert.Equal(path, sendFileFeature.Name);
Assert.Equal(0, sendFileFeature.Offset);
Assert.Null(sendFileFeature.Length);
}
[Theory]
@ -333,8 +330,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -345,7 +343,6 @@ namespace Microsoft.AspNetCore.Mvc
requestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -353,14 +350,11 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status412PreconditionFailed, httpResponse.StatusCode);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.Empty(body);
Assert.Null(sendFileFeature.Name); // Not called
}
[Fact]
@ -375,8 +369,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -387,7 +382,6 @@ namespace Microsoft.AspNetCore.Mvc
requestHeaders.IfModifiedSince = DateTimeOffset.MinValue.AddDays(1);
httpContext.Request.Headers[HeaderNames.Range] = "bytes = 0-6";
httpContext.Request.Method = HttpMethods.Get;
httpContext.Response.Body = new MemoryStream();
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
@ -395,15 +389,12 @@ namespace Microsoft.AspNetCore.Mvc
// Assert
var httpResponse = actionContext.HttpContext.Response;
httpResponse.Body.Seek(0, SeekOrigin.Begin);
var streamReader = new StreamReader(httpResponse.Body);
var body = streamReader.ReadToEndAsync().Result;
Assert.Equal(StatusCodes.Status304NotModified, httpResponse.StatusCode);
Assert.Null(httpResponse.ContentLength);
Assert.Empty(httpResponse.Headers[HeaderNames.ContentRange]);
Assert.NotEmpty(httpResponse.Headers[HeaderNames.LastModified]);
Assert.False(httpResponse.Headers.ContainsKey(HeaderNames.ContentType));
Assert.Empty(body);
Assert.Null(sendFileFeature.Name); // Not called
}
[Fact]
@ -417,8 +408,9 @@ namespace Microsoft.AspNetCore.Mvc
appEnvironment.Setup(app => app.WebRootFileProvider)
.Returns(GetFileProvider(path));
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
httpContext.RequestServices = new ServiceCollection()
.AddSingleton(appEnvironment.Object)
.AddTransient<IActionResultExecutor<VirtualFileResult>, TestVirtualFileResultExecutor>()
@ -428,36 +420,11 @@ namespace Microsoft.AspNetCore.Mvc
// Act
await result.ExecuteResultAsync(context);
httpContext.Response.Body.Position = 0;
// Assert
Assert.NotNull(httpContext.Response.Body);
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
Assert.Equal("FilePathResultTestFile contents¡", contents);
}
[Fact]
public async Task ExecuteResultAsync_FallsbackToStreamCopy_IfNoIHttpSendFilePresent()
{
// Arrange
var path = Path.Combine("TestFiles", "FilePathResultTestFile.txt");
var result = new TestVirtualFileResult(path, "text/plain")
{
FileProvider = GetFileProvider(path),
};
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
httpContext.Response.Body.Position = 0;
// Assert
Assert.NotNull(httpContext.Response.Body);
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
Assert.Equal("FilePathResultTestFile contents¡", contents);
Assert.Equal(path, sendFileFeature.Name);
Assert.Equal(0, sendFileFeature.Offset);
Assert.Null(sendFileFeature.Length);
}
[Fact]
@ -470,7 +437,7 @@ namespace Microsoft.AspNetCore.Mvc
FileProvider = GetFileProvider(path),
};
var sendFileMock = new Mock<IHttpSendFileFeature>();
var sendFileMock = new Mock<IHttpResponseBodyFeature>();
sendFileMock
.Setup(s => s.SendFileAsync(path, 0, null, CancellationToken.None))
.Returns(Task.FromResult<int>(0));
@ -503,7 +470,7 @@ namespace Microsoft.AspNetCore.Mvc
var sendFile = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Features.Set<IHttpSendFileFeature>(sendFile);
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFile);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var appEnvironment = new Mock<IWebHostEnvironment>();
appEnvironment.Setup(app => app.WebRootFileProvider)
@ -548,21 +515,19 @@ namespace Microsoft.AspNetCore.Mvc
"FilePathResultTestFile_ASCII.txt", expectedContentType)
{
FileProvider = GetFileProvider("FilePathResultTestFile_ASCII.txt"),
IsAscii = true,
};
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
var memoryStream = new MemoryStream();
httpContext.Response.Body = memoryStream;
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
// Assert
var contents = Encoding.ASCII.GetString(memoryStream.ToArray());
Assert.Equal("FilePathResultTestFile contents ASCII encoded", contents);
Assert.Equal(expectedContentType, httpContext.Response.ContentType);
Assert.Equal("FilePathResultTestFile_ASCII.txt", sendFileFeature.Name);
}
[Fact]
@ -575,18 +540,16 @@ namespace Microsoft.AspNetCore.Mvc
FileProvider = GetFileProvider(path),
};
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
httpContext.Response.Body = new MemoryStream();
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
httpContext.Response.Body.Position = 0;
// Assert
Assert.NotNull(httpContext.Response.Body);
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
Assert.Equal("FilePathResultTestFile contents¡", contents);
Assert.Equal(path, sendFileFeature.Name);
}
[Theory]
@ -603,20 +566,19 @@ namespace Microsoft.AspNetCore.Mvc
{
FileProvider = GetFileProvider(path),
};
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
var memoryStream = new MemoryStream();
httpContext.Response.Body = memoryStream;
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
httpContext.Response.Body.Position = 0;
// Assert
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
Assert.Equal("FilePathResultTestFile contents¡", contents);
Mock.Get(result.FileProvider).Verify();
Assert.Equal(path, sendFileFeature.Name);
}
[Theory]
@ -633,20 +595,19 @@ namespace Microsoft.AspNetCore.Mvc
{
FileProvider = GetFileProvider(expectedPath),
};
var sendFileFeature = new TestSendFileFeature();
var httpContext = GetHttpContext();
var memoryStream = new MemoryStream();
httpContext.Response.Body = memoryStream;
httpContext.Features.Set<IHttpResponseBodyFeature>(sendFileFeature);
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
// Act
await result.ExecuteResultAsync(context);
httpContext.Response.Body.Position = 0;
// Assert
var contents = await new StreamReader(httpContext.Response.Body).ReadToEndAsync();
Assert.Equal("FilePathResultTestFile contents¡", contents);
Mock.Get(result.FileProvider).Verify();
Assert.Equal(expectedPath, sendFileFeature.Name);
}
[Fact]
@ -760,11 +721,8 @@ namespace Microsoft.AspNetCore.Mvc
public override Task ExecuteResultAsync(ActionContext context)
{
var executor = (TestVirtualFileResultExecutor)context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<VirtualFileResult>>();
executor.IsAscii = IsAscii;
return executor.ExecuteAsync(context, this);
}
public bool IsAscii { get; set; } = false;
}
private class TestVirtualFileResultExecutor : VirtualFileResultExecutor
@ -773,29 +731,29 @@ namespace Microsoft.AspNetCore.Mvc
: base(loggerFactory, hostingEnvironment)
{
}
public bool IsAscii { get; set; }
protected override Stream GetFileStream(IFileInfo fileInfo)
{
if (IsAscii)
{
return new MemoryStream(Encoding.ASCII.GetBytes("FilePathResultTestFile contents ASCII encoded"));
}
else
{
return new MemoryStream(Encoding.UTF8.GetBytes("FilePathResultTestFile contents¡"));
}
}
}
private class TestSendFileFeature : IHttpSendFileFeature
private class TestSendFileFeature : IHttpResponseBodyFeature
{
public string Name { get; set; }
public long Offset { get; set; }
public long? Length { get; set; }
public CancellationToken Token { get; set; }
public Stream Stream => throw new NotImplementedException();
public PipeWriter Writer => throw new NotImplementedException();
public Task CompleteAsync()
{
throw new NotImplementedException();
}
public void DisableBuffering()
{
throw new NotImplementedException();
}
public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
Name = path;
@ -805,6 +763,11 @@ namespace Microsoft.AspNetCore.Mvc
return Task.FromResult(0);
}
public Task StartAsync(CancellationToken cancellation = default)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Pipelines;
using System.Net;
using System.Security.Authentication;
using System.Security.Claims;
@ -24,11 +25,10 @@ namespace Microsoft.AspNetCore.Server.HttpSys
IHttpRequestFeature,
IHttpConnectionFeature,
IHttpResponseFeature,
IHttpSendFileFeature,
IHttpResponseBodyFeature,
ITlsConnectionFeature,
ITlsHandshakeFeature,
// ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231
IHttpBufferingFeature,
IHttpRequestLifetimeFeature,
IHttpAuthenticationFeature,
IHttpUpgradeFeature,
@ -59,6 +59,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
private ClaimsPrincipal _user;
private CancellationToken _disconnectToken;
private Stream _responseStream;
private PipeWriter _pipeWriter;
private bool _bodyCompleted;
private IHeaderDictionary _responseHeaders;
private Fields _initializedFields;
@ -355,12 +357,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
return Request.IsHttps ? this : null;
}
*/
void IHttpBufferingFeature.DisableRequestBuffering()
{
// There is no request buffering.
}
void IHttpBufferingFeature.DisableResponseBuffering()
void IHttpResponseBodyFeature.DisableBuffering()
{
// TODO: What about native buffering?
}
@ -371,6 +369,21 @@ namespace Microsoft.AspNetCore.Server.HttpSys
set { _responseStream = value; }
}
Stream IHttpResponseBodyFeature.Stream => _responseStream;
PipeWriter IHttpResponseBodyFeature.Writer
{
get
{
if (_pipeWriter == null)
{
_pipeWriter = PipeWriter.Create(_responseStream, new StreamPipeWriterOptions(leaveOpen: true));
}
return _pipeWriter;
}
}
IHeaderDictionary IHttpResponseFeature.Headers
{
get { return _responseHeaders; }
@ -419,12 +432,40 @@ namespace Microsoft.AspNetCore.Server.HttpSys
set { Response.StatusCode = value; }
}
async Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
async Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation)
{
await OnResponseStart();
await Response.SendFileAsync(path, offset, length, cancellation);
}
Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellation)
{
return OnResponseStart();
}
Task IHttpResponseBodyFeature.CompleteAsync() => CompleteAsync();
internal async Task CompleteAsync()
{
if (!_responseStarted)
{
await OnResponseStart();
}
if (!_bodyCompleted)
{
_bodyCompleted = true;
if (_pipeWriter != null)
{
// Flush and complete the pipe
await _pipeWriter.CompleteAsync();
}
// Ends the response body.
Response.Dispose();
}
}
CancellationToken IHttpRequestLifetimeFeature.RequestAborted
{
get
@ -514,6 +555,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
_responseStarted = true;
await NotifiyOnStartingAsync();
ConsiderEnablingResponseCache();
Response.Headers.IsReadOnly = true; // Prohibit further modifications.
}
private async Task NotifiyOnStartingAsync()

View File

@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
try
{
await _application.ProcessRequestAsync(context).SupressContext();
await featureContext.OnResponseStart();
await featureContext.CompleteAsync();
}
finally
{
@ -224,6 +224,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
else
{
// We haven't sent a response yet, try to send a 500 Internal Server Error
requestContext.Response.Headers.IsReadOnly = false;
requestContext.Response.Headers.Clear();
SetFatalResponse(requestContext, 500);
}

View File

@ -333,6 +333,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal HttpApiTypes.HTTP_FLAGS ComputeHeaders(long writeCount, bool endOfRequest = false)
{
Headers.IsReadOnly = false; // Temporarily unlock
if (StatusCode == (ushort)StatusCodes.Status401Unauthorized)
{
RequestContext.Server.Options.Authentication.SetAuthenticationChallenge(RequestContext);
@ -418,6 +419,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
flags = HttpApiTypes.HTTP_FLAGS.HTTP_SEND_RESPONSE_FLAG_DISCONNECT;
}
Headers.IsReadOnly = true;
return flags;
}

View File

@ -18,9 +18,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{ typeof(IHttpRequestFeature), _identityFunc },
{ typeof(IHttpConnectionFeature), _identityFunc },
{ typeof(IHttpResponseFeature), _identityFunc },
{ typeof(IHttpSendFileFeature), _identityFunc },
{ typeof(IHttpResponseBodyFeature), _identityFunc },
{ typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() },
{ typeof(IHttpBufferingFeature), _identityFunc },
{ typeof(IHttpRequestLifetimeFeature), _identityFunc },
{ typeof(IHttpAuthenticationFeature), _identityFunc },
{ typeof(IHttpRequestIdentifierFeature), _identityFunc },

View File

@ -9,6 +9,7 @@ using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
@ -18,6 +19,98 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
public class ResponseBodyTests
{
[ConditionalFact]
public async Task ResponseBody_StartAsync_LocksHeadersAndTriggersOnStarting()
{
using (Utilities.CreateHttpServer(out var address, async httpContext =>
{
var startingTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
httpContext.Response.OnStarting(() =>
{
startingTcs.SetResult(0);
return Task.CompletedTask;
});
await httpContext.Response.StartAsync();
Assert.True(httpContext.Response.Headers.IsReadOnly);
await startingTcs.Task.WithTimeout();
await httpContext.Response.WriteAsync("Hello World");
}))
{
var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version);
IEnumerable<string> ignored;
Assert.False(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.True(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
}
}
[ConditionalFact]
public async Task ResponseBody_CompleteAsync_TriggersOnStartingAndLocksHeaders()
{
var responseReceived = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
using (Utilities.CreateHttpServer(out var address, async httpContext =>
{
var startingTcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
httpContext.Response.OnStarting(() =>
{
startingTcs.SetResult(0);
return Task.CompletedTask;
});
await httpContext.Response.CompleteAsync();
Assert.True(httpContext.Response.Headers.IsReadOnly);
await startingTcs.Task.WithTimeout();
await responseReceived.Task.WithTimeout();
}))
{
var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version);
Assert.Equal(0, response.Content.Headers.ContentLength);
responseReceived.SetResult(0);
}
}
[ConditionalFact]
public async Task ResponseBody_CompleteAsync_FlushesThePipe()
{
var responseReceived = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
using (Utilities.CreateHttpServer(out var address, async httpContext =>
{
var writer = httpContext.Response.BodyWriter;
var memory = writer.GetMemory();
writer.Advance(memory.Length);
await httpContext.Response.CompleteAsync();
await responseReceived.Task.WithTimeout();
}))
{
var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version);
Assert.True(0 < (await response.Content.ReadAsByteArrayAsync()).Length);
responseReceived.SetResult(0);
}
}
[ConditionalFact]
public async Task ResponseBody_PipeAdapter_AutomaticallyFlushed()
{
using (Utilities.CreateHttpServer(out var address, httpContext =>
{
var writer = httpContext.Response.BodyWriter;
var memory = writer.GetMemory();
writer.Advance(memory.Length);
return Task.CompletedTask;
}))
{
var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
Assert.Equal(new Version(1, 1), response.Version);
Assert.True(0 < (await response.Content.ReadAsByteArrayAsync()).Length);
}
}
[ConditionalFact]
public async Task ResponseBody_WriteNoHeaders_SetsChunked()
{

View File

@ -162,11 +162,11 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
var responseHeaders = responseInfo.Headers;
var response = httpContext.Response;
var responseHeaders = response.Headers;
responseHeaders["Transfer-Encoding"] = new string[] { "chunked" };
var responseBytes = Encoding.ASCII.GetBytes("10\r\nManually Chunked\r\n0\r\n\r\n");
return responseInfo.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
return response.Body.WriteAsync(responseBytes, 0, responseBytes.Length);
}))
{
using (HttpClient client = new HttpClient())
@ -192,15 +192,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
using (Utilities.CreateHttpServer(out address, httpContext =>
{
httpContext.Features.Get<IHttpBodyControlFeature>().AllowSynchronousIO = true;
var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
var responseHeaders = responseInfo.Headers;
var response = httpContext.Response;
var responseHeaders = response.Headers;
responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" });
responseHeaders.Add("Custom2", new string[] { "value2a, value2b" });
var body = responseInfo.Body;
Assert.False(responseInfo.HasStarted);
var body = response.Body;
Assert.False(response.HasStarted);
body.Flush();
Assert.True(responseInfo.HasStarted);
Assert.Throws<InvalidOperationException>(() => responseInfo.StatusCode = 404);
Assert.True(response.HasStarted);
Assert.Throws<InvalidOperationException>(() => response.StatusCode = 404);
Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
return Task.FromResult(0);
}))
@ -223,15 +223,15 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, async httpContext =>
{
var responseInfo = httpContext.Features.Get<IHttpResponseFeature>();
var responseHeaders = responseInfo.Headers;
var response = httpContext.Response;
var responseHeaders = response.Headers;
responseHeaders.Add("Custom1", new string[] { "value1a", "value1b" });
responseHeaders.Add("Custom2", new string[] { "value2a, value2b" });
var body = responseInfo.Body;
Assert.False(responseInfo.HasStarted);
var body = response.Body;
Assert.False(response.HasStarted);
await body.FlushAsync();
Assert.True(responseInfo.HasStarted);
Assert.Throws<InvalidOperationException>(() => responseInfo.StatusCode = 404);
Assert.True(response.HasStarted);
Assert.Throws<InvalidOperationException>(() => response.StatusCode = 404);
Assert.Throws<InvalidOperationException>(() => responseHeaders.Add("Custom3", new string[] { "value3a, value3b", "value3c" }));
}))
{

View File

@ -31,57 +31,16 @@ namespace Microsoft.AspNetCore.Server.HttpSys
FileLength = new FileInfo(AbsoluteFilePath).Length;
}
[ConditionalFact]
public async Task ResponseSendFile_SupportKeys_Present()
{
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
try
{
/* TODO:
IDictionary<string, object> capabilities = httpContext.Get<IDictionary<string, object>>("server.Capabilities");
Assert.NotNull(capabilities);
Assert.Equal("1.0", capabilities.Get<string>("sendfile.Version"));
IDictionary<string, object> support = capabilities.Get<IDictionary<string, object>>("sendfile.Support");
Assert.NotNull(support);
Assert.Equal("Overlapped", support.Get<string>("sendfile.Concurrency"));
*/
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
Assert.NotNull(sendFile);
}
catch (Exception ex)
{
byte[] body = Encoding.UTF8.GetBytes(ex.ToString());
httpContext.Response.Body.Write(body, 0, body.Length);
}
return Task.FromResult(0);
}))
{
var response = await SendRequestAsync(address);
Assert.Equal(200, (int)response.StatusCode);
IEnumerable<string> ignored;
Assert.True(response.Content.Headers.TryGetValues("content-length", out ignored), "Content-Length");
Assert.False(response.Headers.TransferEncodingChunked.HasValue, "Chunked");
Assert.Equal(0, response.Content.Headers.ContentLength);
Assert.Equal(string.Empty, await response.Content.ReadAsStringAsync());
}
}
[ConditionalFact]
public async Task ResponseSendFile_MissingFile_Throws()
{
var appThrew = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using (Utilities.CreateHttpServer(out var address, httpContext =>
using (Utilities.CreateHttpServer(out var address, async httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
try
{
sendFile.SendFileAsync(string.Empty, 0, null, CancellationToken.None).Wait();
await sendFile.SendFileAsync(string.Empty, 0, null, CancellationToken.None);
appThrew.SetResult(false);
}
catch (Exception)
@ -89,7 +48,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys
appThrew.SetResult(true);
throw;
}
return Task.FromResult(0);
}))
{
HttpResponseMessage response = await SendRequestAsync(address);
@ -104,7 +62,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
}))
{
@ -123,7 +81,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
return sendFile.SendFileAsync(RelativeFilePath, 0, null, CancellationToken.None);
}))
{
@ -142,7 +100,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
}))
{
@ -161,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None).Wait();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
}))
@ -181,7 +139,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, FileLength / 2, CancellationToken.None);
}))
{
@ -201,7 +159,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, async httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
sendFile.SendFileAsync(AbsoluteFilePath, 1234567, null, CancellationToken.None));
completed = true;
@ -220,7 +178,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, async httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
sendFile.SendFileAsync(AbsoluteFilePath, 0, 1234567, CancellationToken.None));
completed = true;
@ -238,7 +196,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
}))
{
@ -257,7 +215,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
httpContext.Response.Headers["Content-lenGth"] = FileLength.ToString();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, null, CancellationToken.None);
}))
@ -278,7 +236,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
httpContext.Response.Headers["Content-lenGth"] = "10";
return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
}))
@ -299,7 +257,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
string address;
using (Utilities.CreateHttpServer(out address, httpContext =>
{
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
httpContext.Response.Headers["Content-lenGth"] = "0";
return sendFile.SendFileAsync(AbsoluteFilePath, 0, 0, CancellationToken.None);
}))
@ -327,7 +285,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
Assert.Same(state, httpContext);
return Task.FromResult(0);
}, httpContext);
var sendFile = httpContext.Features.Get<IHttpSendFileFeature>();
var sendFile = httpContext.Features.Get<IHttpResponseBodyFeature>();
return sendFile.SendFileAsync(AbsoluteFilePath, 0, 10, CancellationToken.None);
}))
{

View File

@ -6,6 +6,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipelines;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
@ -23,11 +24,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
internal partial class IISHttpContext : IFeatureCollection,
IHttpRequestFeature,
IHttpResponseFeature,
IHttpResponseBodyFeature,
IHttpUpgradeFeature,
IHttpRequestLifetimeFeature,
IHttpAuthenticationFeature,
IServerVariablesFeature,
IHttpBufferingFeature,
ITlsConnectionFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature
@ -185,6 +186,48 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
bool IHttpResponseFeature.HasStarted => HasResponseStarted;
Stream IHttpResponseBodyFeature.Stream => ResponseBody;
PipeWriter IHttpResponseBodyFeature.Writer
{
get
{
if (ResponsePipeWrapper == null)
{
ResponsePipeWrapper = PipeWriter.Create(ResponseBody, new StreamPipeWriterOptions(leaveOpen: true));
}
return ResponsePipeWrapper;
}
}
Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken)
{
if (!HasResponseStarted)
{
return InitializeResponse(flushHeaders: false);
}
return Task.CompletedTask;
}
Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
=> SendFileFallback.SendFileAsync(ResponseBody, path, offset, count, cancellation);
Task IHttpResponseBodyFeature.CompleteAsync() => CompleteResponseBodyAsync();
// TODO: In the future this could complete the body all the way down to the server. For now it just ensures
// any unflushed data gets flushed.
protected Task CompleteResponseBodyAsync()
{
if (ResponsePipeWrapper != null)
{
return ResponsePipeWrapper.CompleteAsync().AsTask();
}
return Task.CompletedTask;
}
bool IHttpUpgradeFeature.IsUpgradableRequest => true;
bool IFeatureCollection.IsReadOnly => false;
@ -353,11 +396,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
}
}
void IHttpBufferingFeature.DisableRequestBuffering()
{
}
void IHttpBufferingFeature.DisableResponseBuffering()
void IHttpResponseBodyFeature.DisableBuffering()
{
NativeMethods.HttpDisableBuffering(_pInProcessHandler);
DisableCompression();

View File

@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
{
private static readonly Type IHttpRequestFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestFeature);
private static readonly Type IHttpResponseFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature);
private static readonly Type IHttpResponseBodyFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature);
private static readonly Type IHttpRequestIdentifierFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature);
private static readonly Type IServiceProvidersFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IServiceProvidersFeature);
private static readonly Type IHttpRequestLifetimeFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpRequestLifetimeFeature);
@ -24,14 +25,13 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
private static readonly Type IHttpWebSocketFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpWebSocketFeature);
private static readonly Type ISessionFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.ISessionFeature);
private static readonly Type IHttpBodyControlFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
private static readonly Type IHttpSendFileFeatureType = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
private static readonly Type IISHttpContextType = typeof(IISHttpContext);
private static readonly Type IServerVariablesFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IServerVariablesFeature);
private static readonly Type IHttpBufferingFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature);
private static readonly Type IHttpMaxRequestBodySizeFeature = typeof(global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);
private object _currentIHttpRequestFeature;
private object _currentIHttpResponseFeature;
private object _currentIHttpResponseBodyFeature;
private object _currentIHttpRequestIdentifierFeature;
private object _currentIServiceProvidersFeature;
private object _currentIHttpRequestLifetimeFeature;
@ -46,15 +46,14 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
private object _currentIHttpWebSocketFeature;
private object _currentISessionFeature;
private object _currentIHttpBodyControlFeature;
private object _currentIHttpSendFileFeature;
private object _currentIServerVariablesFeature;
private object _currentIHttpBufferingFeature;
private object _currentIHttpMaxRequestBodySizeFeature;
private void Initialize()
{
_currentIHttpRequestFeature = this;
_currentIHttpResponseFeature = this;
_currentIHttpResponseBodyFeature = this;
_currentIHttpUpgradeFeature = this;
_currentIHttpRequestIdentifierFeature = this;
_currentIHttpRequestLifetimeFeature = this;
@ -62,7 +61,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
_currentIHttpBodyControlFeature = this;
_currentIHttpAuthenticationFeature = this;
_currentIServerVariablesFeature = this;
_currentIHttpBufferingFeature = this;
_currentIHttpMaxRequestBodySizeFeature = this;
_currentITlsConnectionFeature = this;
}
@ -77,6 +75,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
{
return _currentIHttpResponseFeature;
}
if (key == IHttpResponseBodyFeatureType)
{
return _currentIHttpResponseBodyFeature;
}
if (key == IHttpRequestIdentifierFeatureType)
{
return _currentIHttpRequestIdentifierFeature;
@ -133,10 +135,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
{
return _currentIHttpBodyControlFeature;
}
if (key == IHttpSendFileFeatureType)
{
return _currentIHttpSendFileFeature;
}
if (key == IISHttpContextType)
{
return this;
@ -145,10 +143,6 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
{
return _currentIServerVariablesFeature;
}
if (key == IHttpBufferingFeature)
{
return _currentIHttpBufferingFeature;
}
if (key == IHttpMaxRequestBodySizeFeature)
{
return _currentIHttpMaxRequestBodySizeFeature;
@ -171,6 +165,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
_currentIHttpResponseFeature = feature;
return;
}
if (key == IHttpResponseBodyFeatureType)
{
_currentIHttpResponseBodyFeature = feature;
return;
}
if (key == IHttpRequestIdentifierFeatureType)
{
_currentIHttpRequestIdentifierFeature = feature;
@ -241,21 +240,11 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
_currentIHttpBodyControlFeature = feature;
return;
}
if (key == IHttpSendFileFeatureType)
{
_currentIHttpSendFileFeature = feature;
return;
}
if (key == IServerVariablesFeature)
{
_currentIServerVariablesFeature = feature;
return;
}
if (key == IHttpBufferingFeature)
{
_currentIHttpBufferingFeature = feature;
return;
}
if (key == IHttpMaxRequestBodySizeFeature)
{
_currentIHttpMaxRequestBodySizeFeature = feature;
@ -277,6 +266,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
{
yield return new KeyValuePair<Type, object>(IHttpResponseFeatureType, _currentIHttpResponseFeature as global::Microsoft.AspNetCore.Http.Features.IHttpResponseFeature);
}
if (_currentIHttpResponseBodyFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpResponseBodyFeatureType, _currentIHttpResponseBodyFeature as global::Microsoft.AspNetCore.Http.Features.IHttpResponseBodyFeature);
}
if (_currentIHttpRequestIdentifierFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpRequestIdentifierFeatureType, _currentIHttpRequestIdentifierFeature as global::Microsoft.AspNetCore.Http.Features.IHttpRequestIdentifierFeature);
@ -333,18 +326,10 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
{
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBodyControlFeature);
}
if (_currentIHttpSendFileFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature as global::Microsoft.AspNetCore.Http.Features.IHttpSendFileFeature);
}
if (_currentIServerVariablesFeature != null)
{
yield return new KeyValuePair<Type, object>(IServerVariablesFeature, _currentIServerVariablesFeature as global::Microsoft.AspNetCore.Http.Features.IServerVariablesFeature);
}
if (_currentIHttpBufferingFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpBufferingFeature, _currentIHttpBufferingFeature as global::Microsoft.AspNetCore.Http.Features.IHttpBufferingFeature);
}
if (_currentIHttpMaxRequestBodySizeFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpMaxRequestBodySizeFeature, _currentIHttpMaxRequestBodySizeFeature as global::Microsoft.AspNetCore.Http.Features.IHttpMaxRequestBodySizeFeature);

View File

@ -105,6 +105,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
internal WindowsPrincipal WindowsUser { get; set; }
public Stream RequestBody { get; set; }
public Stream ResponseBody { get; set; }
public PipeWriter ResponsePipeWrapper { get; set; }
protected IAsyncIOEngine AsyncIO { get; set; }

View File

@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
}
finally
{
await CompleteResponseBodyAsync();
_streams.Stop();
if (!HasResponseStarted && _applicationException == null && _onStarting != null)

View File

@ -0,0 +1,34 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.AspNetCore.Server.IIS.FunctionalTests.InProcess
{
[Collection(IISTestSiteCollection.Name)]
public class ResponseBodyTests
{
private readonly IISTestSiteFixture _fixture;
public ResponseBodyTests(IISTestSiteFixture fixture)
{
_fixture = fixture;
}
[ConditionalFact]
[RequiresNewHandler]
public async Task ResponseBodyTest_UnflushedPipe_AutoFlushed()
{
Assert.Equal(new byte[10], await _fixture.Client.GetByteArrayAsync($"/UnflushedResponsePipe"));
}
[ConditionalFact]
[RequiresNewHandler]
public async Task ResponseBodyTest_FlushedPipeAndThenUnflushedPipe_AutoFlushed()
{
Assert.Equal(new byte[20], await _fixture.Client.GetByteArrayAsync($"/FlushedPipeAndThenUnflushedPipe"));
}
}
}

View File

@ -371,6 +371,28 @@ namespace TestSite
}
}
#if !FORWARDCOMPAT
private Task UnflushedResponsePipe(HttpContext ctx)
{
var writer = ctx.Response.BodyWriter;
var memory = writer.GetMemory(10);
Assert.True(10 <= memory.Length);
writer.Advance(10);
return Task.CompletedTask;
}
private async Task FlushedPipeAndThenUnflushedPipe(HttpContext ctx)
{
var writer = ctx.Response.BodyWriter;
var memory = writer.GetMemory(10);
Assert.True(10 <= memory.Length);
writer.Advance(10);
await writer.FlushAsync();
memory = writer.GetMemory(10);
Assert.True(10 <= memory.Length);
writer.Advance(10);
}
#endif
private async Task ResponseHeaders(HttpContext ctx)
{
ctx.Response.Headers["UnknownHeader"] = "test123=foo";
@ -521,8 +543,13 @@ namespace TestSite
private async Task ReadAndWriteEchoLinesNoBuffering(HttpContext ctx)
{
#if FORWARDCOMPAT
var feature = ctx.Features.Get<IHttpBufferingFeature>();
feature.DisableResponseBuffering();
#else
var feature = ctx.Features.Get<IHttpResponseBodyFeature>();
feature.DisableBuffering();
#endif
if (ctx.Request.Headers.TryGetValue("Response-Content-Type", out var contentType))
{

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
internal partial class HttpProtocol : IHttpRequestFeature,
IHttpResponseFeature,
IResponseBodyPipeFeature,
IHttpResponseBodyFeature,
IRequestBodyPipeFeature,
IHttpUpgradeFeature,
IHttpConnectionFeature,
@ -28,7 +28,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
IHttpRequestTrailersFeature,
IHttpBodyControlFeature,
IHttpMaxRequestBodySizeFeature,
IHttpResponseStartFeature,
IEndpointFeature,
IRouteValuesFeature
{
@ -236,25 +235,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
set => ResponseBody = value;
}
PipeWriter IResponseBodyPipeFeature.Writer
{
get
{
if (!ReferenceEquals(_responseStreamInternal, ResponseBody))
{
_responseStreamInternal = ResponseBody;
ResponseBodyPipeWriter = PipeWriter.Create(ResponseBody, new StreamPipeWriterOptions(_context.MemoryPool));
OnCompleted((self) =>
{
((PipeWriter)self).Complete();
return Task.CompletedTask;
}, ResponseBodyPipeWriter);
}
return ResponseBodyPipeWriter;
}
}
PipeWriter IHttpResponseBodyFeature.Writer => ResponseBodyPipeWriter;
Endpoint IEndpointFeature.Endpoint
{
@ -268,6 +249,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
set => _routeValues = value;
}
Stream IHttpResponseBodyFeature.Stream => ResponseBody;
protected void ResetHttp1Features()
{
_currentIHttpMinRequestBodyDataRateFeature = this;
@ -277,7 +260,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
protected void ResetHttp2Features()
{
_currentIHttp2StreamIdFeature = this;
_currentIHttpResponseCompletionFeature = this;
_currentIHttpResponseTrailersFeature = this;
_currentIHttpResetFeature = this;
}
@ -327,7 +309,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
ApplicationAbort();
}
Task IHttpResponseStartFeature.StartAsync(CancellationToken cancellationToken)
Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken)
{
if (HasResponseStarted)
{
@ -338,5 +320,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
return InitializeResponseAsync(0);
}
void IHttpResponseBodyFeature.DisableBuffering()
{
}
Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return SendFileFallback.SendFileAsync(ResponseBody, path, offset, count, cancellation);
}
Task IHttpResponseBodyFeature.CompleteAsync()
{
return CompleteAsync();
}
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
private static readonly Type IHttpRequestFeatureType = typeof(IHttpRequestFeature);
private static readonly Type IHttpResponseFeatureType = typeof(IHttpResponseFeature);
private static readonly Type IResponseBodyPipeFeatureType = typeof(IResponseBodyPipeFeature);
private static readonly Type IHttpResponseBodyFeatureType = typeof(IHttpResponseBodyFeature);
private static readonly Type IRequestBodyPipeFeatureType = typeof(IRequestBodyPipeFeature);
private static readonly Type IHttpRequestIdentifierFeatureType = typeof(IHttpRequestIdentifierFeature);
private static readonly Type IServiceProvidersFeatureType = typeof(IServiceProvidersFeature);
@ -29,7 +29,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type IFormFeatureType = typeof(IFormFeature);
private static readonly Type IHttpUpgradeFeatureType = typeof(IHttpUpgradeFeature);
private static readonly Type IHttp2StreamIdFeatureType = typeof(IHttp2StreamIdFeature);
private static readonly Type IHttpResponseCompletionFeatureType = typeof(IHttpResponseCompletionFeature);
private static readonly Type IHttpResponseTrailersFeatureType = typeof(IHttpResponseTrailersFeature);
private static readonly Type IResponseCookiesFeatureType = typeof(IResponseCookiesFeature);
private static readonly Type IItemsFeatureType = typeof(IItemsFeature);
@ -40,13 +39,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private static readonly Type IHttpMinRequestBodyDataRateFeatureType = typeof(IHttpMinRequestBodyDataRateFeature);
private static readonly Type IHttpMinResponseDataRateFeatureType = typeof(IHttpMinResponseDataRateFeature);
private static readonly Type IHttpBodyControlFeatureType = typeof(IHttpBodyControlFeature);
private static readonly Type IHttpResponseStartFeatureType = typeof(IHttpResponseStartFeature);
private static readonly Type IHttpResetFeatureType = typeof(IHttpResetFeature);
private static readonly Type IHttpSendFileFeatureType = typeof(IHttpSendFileFeature);
private object _currentIHttpRequestFeature;
private object _currentIHttpResponseFeature;
private object _currentIResponseBodyPipeFeature;
private object _currentIHttpResponseBodyFeature;
private object _currentIRequestBodyPipeFeature;
private object _currentIHttpRequestIdentifierFeature;
private object _currentIServiceProvidersFeature;
@ -60,7 +57,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentIFormFeature;
private object _currentIHttpUpgradeFeature;
private object _currentIHttp2StreamIdFeature;
private object _currentIHttpResponseCompletionFeature;
private object _currentIHttpResponseTrailersFeature;
private object _currentIResponseCookiesFeature;
private object _currentIItemsFeature;
@ -71,9 +67,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
private object _currentIHttpMinRequestBodyDataRateFeature;
private object _currentIHttpMinResponseDataRateFeature;
private object _currentIHttpBodyControlFeature;
private object _currentIHttpResponseStartFeature;
private object _currentIHttpResetFeature;
private object _currentIHttpSendFileFeature;
private int _featureRevision;
@ -83,7 +77,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpRequestFeature = this;
_currentIHttpResponseFeature = this;
_currentIResponseBodyPipeFeature = this;
_currentIHttpResponseBodyFeature = this;
_currentIRequestBodyPipeFeature = this;
_currentIHttpUpgradeFeature = this;
_currentIHttpRequestIdentifierFeature = this;
@ -93,7 +87,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIHttpMaxRequestBodySizeFeature = this;
_currentIHttpMinRequestBodyDataRateFeature = this;
_currentIHttpBodyControlFeature = this;
_currentIHttpResponseStartFeature = this;
_currentIRouteValuesFeature = this;
_currentIEndpointFeature = this;
@ -102,7 +95,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentIQueryFeature = null;
_currentIFormFeature = null;
_currentIHttp2StreamIdFeature = null;
_currentIHttpResponseCompletionFeature = null;
_currentIHttpResponseTrailersFeature = null;
_currentIResponseCookiesFeature = null;
_currentIItemsFeature = null;
@ -111,7 +103,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
_currentISessionFeature = null;
_currentIHttpMinResponseDataRateFeature = null;
_currentIHttpResetFeature = null;
_currentIHttpSendFileFeature = null;
}
// Internal for testing
@ -174,9 +165,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = _currentIHttpResponseFeature;
}
else if (key == IResponseBodyPipeFeatureType)
else if (key == IHttpResponseBodyFeatureType)
{
feature = _currentIResponseBodyPipeFeature;
feature = _currentIHttpResponseBodyFeature;
}
else if (key == IRequestBodyPipeFeatureType)
{
@ -230,10 +221,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = _currentIHttp2StreamIdFeature;
}
else if (key == IHttpResponseCompletionFeatureType)
{
feature = _currentIHttpResponseCompletionFeature;
}
else if (key == IHttpResponseTrailersFeatureType)
{
feature = _currentIHttpResponseTrailersFeature;
@ -274,18 +261,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = _currentIHttpBodyControlFeature;
}
else if (key == IHttpResponseStartFeatureType)
{
feature = _currentIHttpResponseStartFeature;
}
else if (key == IHttpResetFeatureType)
{
feature = _currentIHttpResetFeature;
}
else if (key == IHttpSendFileFeatureType)
{
feature = _currentIHttpSendFileFeature;
}
else if (MaybeExtra != null)
{
feature = ExtraFeatureGet(key);
@ -306,9 +285,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpResponseFeature = value;
}
else if (key == IResponseBodyPipeFeatureType)
else if (key == IHttpResponseBodyFeatureType)
{
_currentIResponseBodyPipeFeature = value;
_currentIHttpResponseBodyFeature = value;
}
else if (key == IRequestBodyPipeFeatureType)
{
@ -362,10 +341,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttp2StreamIdFeature = value;
}
else if (key == IHttpResponseCompletionFeatureType)
{
_currentIHttpResponseCompletionFeature = value;
}
else if (key == IHttpResponseTrailersFeatureType)
{
_currentIHttpResponseTrailersFeature = value;
@ -406,18 +381,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpBodyControlFeature = value;
}
else if (key == IHttpResponseStartFeatureType)
{
_currentIHttpResponseStartFeature = value;
}
else if (key == IHttpResetFeatureType)
{
_currentIHttpResetFeature = value;
}
else if (key == IHttpSendFileFeatureType)
{
_currentIHttpSendFileFeature = value;
}
else
{
ExtraFeatureSet(key, value);
@ -436,9 +403,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = (TFeature)_currentIHttpResponseFeature;
}
else if (typeof(TFeature) == typeof(IResponseBodyPipeFeature))
else if (typeof(TFeature) == typeof(IHttpResponseBodyFeature))
{
feature = (TFeature)_currentIResponseBodyPipeFeature;
feature = (TFeature)_currentIHttpResponseBodyFeature;
}
else if (typeof(TFeature) == typeof(IRequestBodyPipeFeature))
{
@ -492,10 +459,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = (TFeature)_currentIHttp2StreamIdFeature;
}
else if (typeof(TFeature) == typeof(IHttpResponseCompletionFeature))
{
feature = (TFeature)_currentIHttpResponseCompletionFeature;
}
else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
{
feature = (TFeature)_currentIHttpResponseTrailersFeature;
@ -536,18 +499,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
feature = (TFeature)_currentIHttpBodyControlFeature;
}
else if (typeof(TFeature) == typeof(IHttpResponseStartFeature))
{
feature = (TFeature)_currentIHttpResponseStartFeature;
}
else if (typeof(TFeature) == typeof(IHttpResetFeature))
{
feature = (TFeature)_currentIHttpResetFeature;
}
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
{
feature = (TFeature)_currentIHttpSendFileFeature;
}
else if (MaybeExtra != null)
{
feature = (TFeature)(ExtraFeatureGet(typeof(TFeature)));
@ -572,9 +527,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpResponseFeature = feature;
}
else if (typeof(TFeature) == typeof(IResponseBodyPipeFeature))
else if (typeof(TFeature) == typeof(IHttpResponseBodyFeature))
{
_currentIResponseBodyPipeFeature = feature;
_currentIHttpResponseBodyFeature = feature;
}
else if (typeof(TFeature) == typeof(IRequestBodyPipeFeature))
{
@ -628,10 +583,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttp2StreamIdFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpResponseCompletionFeature))
{
_currentIHttpResponseCompletionFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpResponseTrailersFeature))
{
_currentIHttpResponseTrailersFeature = feature;
@ -672,18 +623,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
_currentIHttpBodyControlFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpResponseStartFeature))
{
_currentIHttpResponseStartFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpResetFeature))
{
_currentIHttpResetFeature = feature;
}
else if (typeof(TFeature) == typeof(IHttpSendFileFeature))
{
_currentIHttpSendFileFeature = feature;
}
else
{
ExtraFeatureSet(typeof(TFeature), feature);
@ -700,9 +643,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair<Type, object>(IHttpResponseFeatureType, _currentIHttpResponseFeature);
}
if (_currentIResponseBodyPipeFeature != null)
if (_currentIHttpResponseBodyFeature != null)
{
yield return new KeyValuePair<Type, object>(IResponseBodyPipeFeatureType, _currentIResponseBodyPipeFeature);
yield return new KeyValuePair<Type, object>(IHttpResponseBodyFeatureType, _currentIHttpResponseBodyFeature);
}
if (_currentIRequestBodyPipeFeature != null)
{
@ -756,10 +699,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair<Type, object>(IHttp2StreamIdFeatureType, _currentIHttp2StreamIdFeature);
}
if (_currentIHttpResponseCompletionFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpResponseCompletionFeatureType, _currentIHttpResponseCompletionFeature);
}
if (_currentIHttpResponseTrailersFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpResponseTrailersFeatureType, _currentIHttpResponseTrailersFeature);
@ -800,18 +739,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
{
yield return new KeyValuePair<Type, object>(IHttpBodyControlFeatureType, _currentIHttpBodyControlFeature);
}
if (_currentIHttpResponseStartFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpResponseStartFeatureType, _currentIHttpResponseStartFeature);
}
if (_currentIHttpResetFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpResetFeatureType, _currentIHttpResetFeature);
}
if (_currentIHttpSendFileFeature != null)
{
yield return new KeyValuePair<Type, object>(IHttpSendFileFeatureType, _currentIHttpSendFileFeature);
}
if (MaybeExtra != null)
{

View File

@ -14,7 +14,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
internal partial class Http2Stream : IHttp2StreamIdFeature,
IHttpMinRequestBodyDataRateFeature,
IHttpResetFeature,
IHttpResponseCompletionFeature,
IHttpResponseTrailersFeature
{
@ -56,11 +55,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
}
}
Task IHttpResponseCompletionFeature.CompleteAsync()
{
return CompleteAsync();
}
void IHttpResetFeature.Reset(int errorCode)
{
var abortReason = new ConnectionAbortedException(CoreStrings.FormatHttp2StreamResetByApplication((Http2ErrorCode)errorCode));

View File

@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
_collection[typeof(IHttpRequestFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseFeature)] = CreateHttp1Connection();
_collection[typeof(IResponseBodyPipeFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseBodyFeature)] = CreateHttp1Connection();
_collection[typeof(IRequestBodyPipeFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpRequestIdentifierFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpRequestLifetimeFeature)] = CreateHttp1Connection();
@ -127,7 +127,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection[typeof(IHttpMinRequestBodyDataRateFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpMinResponseDataRateFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpBodyControlFeature)] = CreateHttp1Connection();
_collection[typeof(IHttpResponseStartFeature)] = CreateHttp1Connection();
_collection[typeof(IRouteValuesFeature)] = CreateHttp1Connection();
_collection[typeof(IEndpointFeature)] = CreateHttp1Connection();
@ -141,7 +140,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
_collection.Set<IHttpRequestFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseFeature>(CreateHttp1Connection());
_collection.Set<IResponseBodyPipeFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseBodyFeature>(CreateHttp1Connection());
_collection.Set<IRequestBodyPipeFeature>(CreateHttp1Connection());
_collection.Set<IHttpRequestIdentifierFeature>(CreateHttp1Connection());
_collection.Set<IHttpRequestLifetimeFeature>(CreateHttp1Connection());
@ -151,7 +150,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
_collection.Set<IHttpMinRequestBodyDataRateFeature>(CreateHttp1Connection());
_collection.Set<IHttpMinResponseDataRateFeature>(CreateHttp1Connection());
_collection.Set<IHttpBodyControlFeature>(CreateHttp1Connection());
_collection.Set<IHttpResponseStartFeature>(CreateHttp1Connection());
_collection.Set<IRouteValuesFeature>(CreateHttp1Connection());
_collection.Set<IEndpointFeature>(CreateHttp1Connection());
@ -197,7 +195,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
{
Assert.Same(_collection.Get<IHttpRequestFeature>(), _collection[typeof(IHttpRequestFeature)]);
Assert.Same(_collection.Get<IHttpResponseFeature>(), _collection[typeof(IHttpResponseFeature)]);
Assert.Same(_collection.Get<IResponseBodyPipeFeature>(), _collection[typeof(IResponseBodyPipeFeature)]);
Assert.Same(_collection.Get<IHttpResponseBodyFeature>(), _collection[typeof(IHttpResponseBodyFeature)]);
Assert.Same(_collection.Get<IRequestBodyPipeFeature>(), _collection[typeof(IRequestBodyPipeFeature)]);
Assert.Same(_collection.Get<IHttpRequestIdentifierFeature>(), _collection[typeof(IHttpRequestIdentifierFeature)]);
Assert.Same(_collection.Get<IHttpRequestLifetimeFeature>(), _collection[typeof(IHttpRequestLifetimeFeature)]);
@ -206,7 +204,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
Assert.Same(_collection.Get<IHttpMinRequestBodyDataRateFeature>(), _collection[typeof(IHttpMinRequestBodyDataRateFeature)]);
Assert.Same(_collection.Get<IHttpMinResponseDataRateFeature>(), _collection[typeof(IHttpMinResponseDataRateFeature)]);
Assert.Same(_collection.Get<IHttpBodyControlFeature>(), _collection[typeof(IHttpBodyControlFeature)]);
Assert.Same(_collection.Get<IHttpResponseStartFeature>(), _collection[typeof(IHttpResponseStartFeature)]);
}
private int EachHttpProtocolFeatureSetAndUnique()

View File

@ -33,20 +33,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
return _collection.Get<IHttpRequestFeature>();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public IHttpSendFileFeature GetViaTypeOf_Last()
{
return (IHttpSendFileFeature)_collection[typeof(IHttpSendFileFeature)];
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public IHttpSendFileFeature GetViaGeneric_Last()
{
return _collection.Get<IHttpSendFileFeature>();
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public object GetViaTypeOf_Custom()
@ -103,14 +89,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance
_collection = http1Connection;
}
private class SendFileFeature : IHttpSendFileFeature
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
throw new NotImplementedException();
}
}
private interface IHttpCustomFeature
{
}

View File

@ -3390,10 +3390,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
Assert.True(context.Response.Headers.IsReadOnly);
@ -3446,12 +3443,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
context.Response.AppendTrailer("CustomName", "Custom Value");
await completionFeature.CompleteAsync().DefaultTimeout();
await completionFeature.CompleteAsync().DefaultTimeout(); // Can be called twice, no-ops
await context.Response.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout(); // Can be called twice, no-ops
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
Assert.True(context.Response.Headers.IsReadOnly);
@ -3514,13 +3509,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
context.Response.ContentLength = 25;
context.Response.AppendTrailer("CustomName", "Custom Value");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => completionFeature.CompleteAsync().DefaultTimeout());
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.Response.CompleteAsync().DefaultTimeout());
Assert.Equal(CoreStrings.FormatTooFewBytesWritten(0, 25), ex.Message);
Assert.True(startingTcs.Task.IsCompletedSuccessfully);
@ -3571,15 +3564,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await context.Response.WriteAsync("Hello World");
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
Assert.True(context.Response.Headers.IsReadOnly);
await completionFeature.CompleteAsync().DefaultTimeout();
await completionFeature.CompleteAsync().DefaultTimeout(); // Can be called twice, no-ops
await context.Response.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout(); // Can be called twice, no-ops
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
@ -3639,10 +3630,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
Assert.True(context.Response.Headers.IsReadOnly);
@ -3698,14 +3686,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await context.Response.WriteAsync("Hello World").DefaultTimeout();
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
Assert.True(context.Response.Headers.IsReadOnly);
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
@ -3768,8 +3754,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
var buffer = context.Response.BodyWriter.GetMemory();
var length = Encoding.UTF8.GetBytes("Hello World", buffer.Span);
@ -3780,7 +3764,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
context.Response.AppendTrailer("CustomName", "Custom Value");
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
Assert.True(context.Response.Headers.IsReadOnly);
@ -3849,8 +3833,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await context.Response.WriteAsync("Hello World");
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
@ -3858,7 +3840,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
context.Response.AppendTrailer("CustomName", "Custom Value");
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
@ -3925,8 +3907,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
context.Response.ContentLength = 25;
await context.Response.WriteAsync("Hello World");
@ -3935,7 +3915,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
context.Response.AppendTrailer("CustomName", "Custom Value");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => completionFeature.CompleteAsync().DefaultTimeout());
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.Response.CompleteAsync().DefaultTimeout());
Assert.Equal(CoreStrings.FormatTooFewBytesWritten(11, 25), ex.Message);
Assert.False(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
@ -4068,8 +4048,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await context.Response.WriteAsync("Hello World");
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
@ -4077,7 +4055,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
context.Response.AppendTrailer("CustomName", "Custom Value");
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
@ -4151,8 +4129,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var requestBodyTask = context.Request.BodyReader.ReadAsync();
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await context.Response.WriteAsync("Hello World");
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
@ -4160,7 +4136,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
context.Response.AppendTrailer("CustomName", "Custom Value");
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
@ -4235,8 +4211,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
try
{
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await context.Response.WriteAsync("Hello World");
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
@ -4244,7 +4218,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
context.Response.AppendTrailer("CustomName", "Custom Value");
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);
@ -4321,8 +4295,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
var requestBodyTask = context.Request.BodyReader.ReadAsync();
context.Response.OnStarting(() => { startingTcs.SetResult(0); return Task.CompletedTask; });
var completionFeature = context.Features.Get<IHttpResponseCompletionFeature>();
Assert.NotNull(completionFeature);
await context.Response.WriteAsync("Hello World");
Assert.True(startingTcs.Task.IsCompletedSuccessfully); // OnStarting got called.
@ -4330,7 +4302,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
context.Response.AppendTrailer("CustomName", "Custom Value");
await completionFeature.CompleteAsync().DefaultTimeout();
await context.Response.CompleteAsync().DefaultTimeout();
Assert.True(context.Features.Get<IHttpResponseTrailersFeature>().Trailers.IsReadOnly);

View File

@ -13,7 +13,7 @@ namespace CodeGenerator
{
"IHttpRequestFeature",
"IHttpResponseFeature",
"IResponseBodyPipeFeature",
"IHttpResponseBodyFeature",
"IRequestBodyPipeFeature",
"IHttpRequestIdentifierFeature",
"IServiceProvidersFeature",
@ -35,7 +35,6 @@ namespace CodeGenerator
{
"IHttpUpgradeFeature",
"IHttp2StreamIdFeature",
"IHttpResponseCompletionFeature",
"IHttpResponseTrailersFeature",
"IResponseCookiesFeature",
"IItemsFeature",
@ -46,19 +45,12 @@ namespace CodeGenerator
"IHttpMinRequestBodyDataRateFeature",
"IHttpMinResponseDataRateFeature",
"IHttpBodyControlFeature",
"IHttpResponseStartFeature",
"IHttpResetFeature"
};
var rareFeatures = new[]
{
"IHttpSendFileFeature",
};
var allFeatures = alwaysFeatures
.Concat(commonFeatures)
.Concat(sometimesFeatures)
.Concat(rareFeatures)
.ToArray();
// NOTE: This list MUST always match the set of feature interfaces implemented by HttpProtocol.
@ -67,7 +59,7 @@ namespace CodeGenerator
{
"IHttpRequestFeature",
"IHttpResponseFeature",
"IResponseBodyPipeFeature",
"IHttpResponseBodyFeature",
"IRequestBodyPipeFeature",
"IHttpUpgradeFeature",
"IHttpRequestIdentifierFeature",
@ -77,7 +69,6 @@ namespace CodeGenerator
"IHttpMaxRequestBodySizeFeature",
"IHttpMinRequestBodyDataRateFeature",
"IHttpBodyControlFeature",
"IHttpResponseStartFeature",
"IRouteValuesFeature",
"IEndpointFeature"
};

View File

@ -28,6 +28,13 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
return "/";
}
// OPTIONS *
// RemoveDotSegments Asserts path always starts with a '/'
if (rawPath.Length == 1 && rawPath[0] == (byte)'*')
{
return "*";
}
var unescapedPath = Unescape(rawPath);
var length = PathNormalizer.RemoveDotSegments(unescapedPath);

View File

@ -582,6 +582,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
var features = new FeatureCollection();
features.Set<IHttpRequestFeature>(requestFeature);
features.Set<IHttpResponseFeature>(responseFeature);
features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(Stream.Null));
features.Set<IHttpConnectionFeature>(connectionFeature);
// REVIEW: We could strategically look at adding other features but it might be better

View File

@ -32,8 +32,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal.Transports
context.Response.Headers[HeaderNames.CacheControl] = "no-cache";
// Make sure we disable all response buffering for SSE
var bufferingFeature = context.Features.Get<IHttpBufferingFeature>();
bufferingFeature?.DisableResponseBuffering();
var bufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
bufferingFeature.DisableBuffering();
context.Response.Headers[HeaderNames.ContentEncoding] = "identity";

View File

@ -44,8 +44,8 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
var connection = new DefaultConnectionContext("foo", pair.Transport, pair.Application);
var context = new DefaultHttpContext();
var feature = new HttpBufferingFeature();
context.Features.Set<IHttpBufferingFeature>(feature);
var feature = new HttpBufferingFeature(new MemoryStream());
context.Features.Set<IHttpResponseBodyFeature>(feature);
var sse = new ServerSentEventsServerTransport(connection.Application.Input, connectionId: connection.ConnectionId, LoggerFactory);
connection.Transport.Output.Complete();
@ -129,18 +129,13 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
}
}
private class HttpBufferingFeature : IHttpBufferingFeature
private class HttpBufferingFeature : StreamResponseBodyFeature
{
public bool RequestBufferingDisabled { get; set; }
public bool ResponseBufferingDisabled { get; set; }
public void DisableRequestBuffering()
{
RequestBufferingDisabled = true;
}
public HttpBufferingFeature(Stream stream) : base(stream) { }
public void DisableResponseBuffering()
public override void DisableBuffering()
{
ResponseBufferingDisabled = true;
}