Unified response body features (#12328)
This commit is contained in:
parent
2884ef6e1f
commit
080660967b
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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) { }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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" }));
|
||||
}))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ namespace Microsoft.AspNetCore.Server.IIS.Core
|
|||
}
|
||||
finally
|
||||
{
|
||||
await CompleteResponseBodyAsync();
|
||||
_streams.Stop();
|
||||
|
||||
if (!HasResponseStarted && _applicationException == null && _onStarting != null)
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue