Check Accept-Encoding headers before creating compression provider (#154)
This commit is contained in:
Родитель
67c93a9acd
Коммит
f0b44ac7b5
|
@ -17,23 +17,24 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature
|
internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature
|
||||||
{
|
{
|
||||||
private readonly HttpResponse _response;
|
private readonly HttpContext _context;
|
||||||
private readonly Stream _bodyOriginalStream;
|
private readonly Stream _bodyOriginalStream;
|
||||||
private readonly IResponseCompressionProvider _provider;
|
private readonly IResponseCompressionProvider _provider;
|
||||||
private readonly ICompressionProvider _compressionProvider;
|
|
||||||
private readonly IHttpBufferingFeature _innerBufferFeature;
|
private readonly IHttpBufferingFeature _innerBufferFeature;
|
||||||
private readonly IHttpSendFileFeature _innerSendFileFeature;
|
private readonly IHttpSendFileFeature _innerSendFileFeature;
|
||||||
|
|
||||||
|
private ICompressionProvider _compressionProvider = null;
|
||||||
private bool _compressionChecked = false;
|
private bool _compressionChecked = false;
|
||||||
private Stream _compressionStream = null;
|
private Stream _compressionStream = null;
|
||||||
|
private bool _providerCreated = false;
|
||||||
|
private bool _autoFlush = false;
|
||||||
|
|
||||||
internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, IResponseCompressionProvider provider, ICompressionProvider compressionProvider,
|
internal BodyWrapperStream(HttpContext context, Stream bodyOriginalStream, IResponseCompressionProvider provider,
|
||||||
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature)
|
IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature)
|
||||||
{
|
{
|
||||||
_response = response;
|
_context = context;
|
||||||
_bodyOriginalStream = bodyOriginalStream;
|
_bodyOriginalStream = bodyOriginalStream;
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
_compressionProvider = compressionProvider;
|
|
||||||
_innerBufferFeature = innerBufferFeature;
|
_innerBufferFeature = innerBufferFeature;
|
||||||
_innerSendFileFeature = innerSendFileFeature;
|
_innerSendFileFeature = innerSendFileFeature;
|
||||||
}
|
}
|
||||||
|
@ -125,6 +126,10 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
if (_compressionStream != null)
|
if (_compressionStream != null)
|
||||||
{
|
{
|
||||||
_compressionStream.Write(buffer, offset, count);
|
_compressionStream.Write(buffer, offset, count);
|
||||||
|
if (_autoFlush)
|
||||||
|
{
|
||||||
|
_compressionStream.Flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -133,44 +138,70 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET451
|
#if NET451
|
||||||
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, Object state)
|
||||||
{
|
{
|
||||||
OnWrite();
|
var tcs = new TaskCompletionSource<object>(state);
|
||||||
|
InternalWriteAsync(buffer, offset, count, callback, tcs);
|
||||||
if (_compressionStream != null)
|
return tcs.Task;
|
||||||
{
|
}
|
||||||
return _compressionStream.BeginWrite(buffer, offset, count, callback, state);
|
|
||||||
|
private async void InternalWriteAsync(byte[] buffer, int offset, int count, AsyncCallback callback, TaskCompletionSource<object> tcs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await WriteAsync(buffer, offset, count);
|
||||||
|
tcs.TrySetResult(null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
tcs.TrySetException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (callback != null)
|
||||||
|
{
|
||||||
|
// Offload callbacks to avoid stack dives on sync completions.
|
||||||
|
var ignored = Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
callback(tcs.Task);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Suppress exceptions on background threads.
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return _bodyOriginalStream.BeginWrite(buffer, offset, count, callback, state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void EndWrite(IAsyncResult asyncResult)
|
public override void EndWrite(IAsyncResult asyncResult)
|
||||||
{
|
{
|
||||||
if (!_compressionChecked)
|
if (asyncResult == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("BeginWrite was not called before EndWrite");
|
throw new ArgumentNullException(nameof(asyncResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_compressionStream != null)
|
var task = (Task)asyncResult;
|
||||||
{
|
task.GetAwaiter().GetResult();
|
||||||
_compressionStream.EndWrite(asyncResult);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_bodyOriginalStream.EndWrite(asyncResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
OnWrite();
|
OnWrite();
|
||||||
|
|
||||||
if (_compressionStream != null)
|
if (_compressionStream != null)
|
||||||
{
|
{
|
||||||
return _compressionStream.WriteAsync(buffer, offset, count, cancellationToken);
|
await _compressionStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
|
if (_autoFlush)
|
||||||
|
{
|
||||||
|
await _compressionStream.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
}
|
}
|
||||||
return _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWrite()
|
private void OnWrite()
|
||||||
|
@ -178,22 +209,30 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
if (!_compressionChecked)
|
if (!_compressionChecked)
|
||||||
{
|
{
|
||||||
_compressionChecked = true;
|
_compressionChecked = true;
|
||||||
|
if (_provider.ShouldCompressResponse(_context))
|
||||||
if (IsCompressable())
|
|
||||||
{
|
{
|
||||||
_response.Headers.Append(HeaderNames.ContentEncoding, _compressionProvider.EncodingName);
|
var compressionProvider = ResolveCompressionProvider();
|
||||||
_response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
|
if (compressionProvider != null)
|
||||||
_response.Headers.Remove(HeaderNames.ContentLength);
|
|
||||||
|
|
||||||
_compressionStream = _compressionProvider.CreateStream(_bodyOriginalStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsCompressable()
|
|
||||||
{
|
{
|
||||||
return !_response.Headers.ContainsKey(HeaderNames.ContentRange) && // The response is not partial
|
_context.Response.Headers.Append(HeaderNames.ContentEncoding, compressionProvider.EncodingName);
|
||||||
_provider.ShouldCompressResponse(_response.HttpContext);
|
_context.Response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
|
||||||
|
_context.Response.Headers.Remove(HeaderNames.ContentLength);
|
||||||
|
|
||||||
|
_compressionStream = compressionProvider.CreateStream(_bodyOriginalStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICompressionProvider ResolveCompressionProvider()
|
||||||
|
{
|
||||||
|
if (!_providerCreated)
|
||||||
|
{
|
||||||
|
_providerCreated = true;
|
||||||
|
_compressionProvider = _provider.GetCompressionProvider(_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _compressionProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisableRequestBuffering()
|
public void DisableRequestBuffering()
|
||||||
|
@ -205,13 +244,16 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
// For this to be effective it needs to be called before the first write.
|
// For this to be effective it needs to be called before the first write.
|
||||||
public void DisableResponseBuffering()
|
public void DisableResponseBuffering()
|
||||||
{
|
{
|
||||||
if (!_compressionProvider.SupportsFlush)
|
if (ResolveCompressionProvider()?.SupportsFlush == false)
|
||||||
{
|
{
|
||||||
// Don't compress, some of the providers don't implement Flush (e.g. .NET 4.5.1 GZip/Deflate stream)
|
// Don't compress, some of the providers don't implement Flush (e.g. .NET 4.5.1 GZip/Deflate stream)
|
||||||
// which would block real-time responses like SignalR.
|
// which would block real-time responses like SignalR.
|
||||||
_compressionChecked = true;
|
_compressionChecked = true;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_autoFlush = true;
|
||||||
|
}
|
||||||
_innerBufferFeature?.DisableResponseBuffering();
|
_innerBufferFeature?.DisableResponseBuffering();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,6 +299,11 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
fileStream.Seek(offset, SeekOrigin.Begin);
|
fileStream.Seek(offset, SeekOrigin.Begin);
|
||||||
await StreamCopyOperation.CopyToAsync(fileStream, _compressionStream, count, cancellation);
|
await StreamCopyOperation.CopyToAsync(fileStream, _compressionStream, count, cancellation);
|
||||||
|
|
||||||
|
if (_autoFlush)
|
||||||
|
{
|
||||||
|
await _compressionStream.FlushAsync(cancellation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,5 +23,12 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
/// <param name="context"></param>
|
/// <param name="context"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
bool ShouldCompressResponse(HttpContext context);
|
bool ShouldCompressResponse(HttpContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Examines the request to see if compression should be used for response.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
bool CheckRequestAcceptsCompression(HttpContext context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Resources;
|
using System.Resources;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCompression.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
||||||
[assembly: AssemblyMetadata("Serviceable", "True")]
|
[assembly: AssemblyMetadata("Serviceable", "True")]
|
||||||
[assembly: NeutralResourcesLanguage("en-us")]
|
[assembly: NeutralResourcesLanguage("en-us")]
|
||||||
|
|
|
@ -18,15 +18,13 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
|
|
||||||
private readonly IResponseCompressionProvider _provider;
|
private readonly IResponseCompressionProvider _provider;
|
||||||
|
|
||||||
private readonly bool _enableForHttps;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initialize the Response Compression middleware.
|
/// Initialize the Response Compression middleware.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="next"></param>
|
/// <param name="next"></param>
|
||||||
/// <param name="provider"></param>
|
/// <param name="provider"></param>
|
||||||
/// <param name="options"></param>
|
public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider)
|
||||||
public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider, IOptions<ResponseCompressionOptions> options)
|
|
||||||
{
|
{
|
||||||
if (next == null)
|
if (next == null)
|
||||||
{
|
{
|
||||||
|
@ -36,14 +34,9 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(provider));
|
throw new ArgumentNullException(nameof(provider));
|
||||||
}
|
}
|
||||||
if (options == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(options));
|
|
||||||
}
|
|
||||||
|
|
||||||
_next = next;
|
_next = next;
|
||||||
_provider = provider;
|
_provider = provider;
|
||||||
_enableForHttps = options.Value.EnableForHttps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -53,14 +46,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
ICompressionProvider compressionProvider = null;
|
if (!_provider.CheckRequestAcceptsCompression(context))
|
||||||
|
|
||||||
if (!context.Request.IsHttps || _enableForHttps)
|
|
||||||
{
|
|
||||||
compressionProvider = _provider.GetCompressionProvider(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (compressionProvider == null)
|
|
||||||
{
|
{
|
||||||
await _next(context);
|
await _next(context);
|
||||||
return;
|
return;
|
||||||
|
@ -70,7 +56,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
|
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
|
||||||
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
|
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
|
||||||
|
|
||||||
var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _provider, compressionProvider,
|
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
|
||||||
originalBufferFeature, originalSendFileFeature);
|
originalBufferFeature, originalSendFileFeature);
|
||||||
context.Response.Body = bodyWrapperStream;
|
context.Response.Body = bodyWrapperStream;
|
||||||
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
|
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
|
||||||
|
|
|
@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
{
|
{
|
||||||
private readonly ICompressionProvider[] _providers;
|
private readonly ICompressionProvider[] _providers;
|
||||||
private readonly HashSet<string> _mimeTypes;
|
private readonly HashSet<string> _mimeTypes;
|
||||||
|
private readonly bool _enableForHttps;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If no compression providers are specified then GZip is used by default.
|
/// If no compression providers are specified then GZip is used by default.
|
||||||
|
@ -54,6 +55,8 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
mimeTypes = ResponseCompressionDefaults.MimeTypes;
|
mimeTypes = ResponseCompressionDefaults.MimeTypes;
|
||||||
}
|
}
|
||||||
_mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
|
_mimeTypes = new HashSet<string>(mimeTypes, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_enableForHttps = options.Value.EnableForHttps;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -103,6 +106,11 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual bool ShouldCompressResponse(HttpContext context)
|
public virtual bool ShouldCompressResponse(HttpContext context)
|
||||||
{
|
{
|
||||||
|
if (context.Response.Headers.ContainsKey(HeaderNames.ContentRange))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var mimeType = context.Response.ContentType;
|
var mimeType = context.Response.ContentType;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(mimeType))
|
if (string.IsNullOrEmpty(mimeType))
|
||||||
|
@ -121,5 +129,15 @@ namespace Microsoft.AspNetCore.ResponseCompression
|
||||||
// TODO PERF: StringSegments?
|
// TODO PERF: StringSegments?
|
||||||
return _mimeTypes.Contains(mimeType);
|
return _mimeTypes.Contains(mimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool CheckRequestAcceptsCompression(HttpContext context)
|
||||||
|
{
|
||||||
|
if (context.Request.IsHttps && !_enableForHttps)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,211 @@
|
||||||
|
// 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 Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCompression.Tests
|
||||||
|
{
|
||||||
|
public class BodyWrapperStreamTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void Write_IsPassedToUnderlyingStream_WhenDisableResponseBuffering(bool flushable)
|
||||||
|
{
|
||||||
|
|
||||||
|
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 stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(flushable), null, null);
|
||||||
|
|
||||||
|
stream.DisableResponseBuffering();
|
||||||
|
stream.Write(buffer, 0, buffer.Length);
|
||||||
|
|
||||||
|
Assert.Equal(buffer, written);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public async Task WriteAsync_IsPassedToUnderlyingStream_WhenDisableResponseBuffering(bool flushable)
|
||||||
|
{
|
||||||
|
var buffer = new byte[] { 1 };
|
||||||
|
byte[] written = null;
|
||||||
|
|
||||||
|
var mock = new Mock<Stream>();
|
||||||
|
mock.SetupGet(s => s.CanWrite).Returns(true);
|
||||||
|
mock.Setup(s => s.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<byte[], int, int, CancellationToken>((b, o, c, t) =>
|
||||||
|
{
|
||||||
|
written = new ArraySegment<byte>(b, 0, c).ToArray();
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(0));
|
||||||
|
|
||||||
|
var stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(flushable), null, null);
|
||||||
|
|
||||||
|
stream.DisableResponseBuffering();
|
||||||
|
await stream.WriteAsync(buffer, 0, buffer.Length);
|
||||||
|
|
||||||
|
Assert.Equal(buffer, written);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendFileAsync_IsPassedToUnderlyingStream_WhenDisableResponseBuffering()
|
||||||
|
{
|
||||||
|
byte[] written = null;
|
||||||
|
|
||||||
|
var mock = new Mock<Stream>();
|
||||||
|
mock.SetupGet(s => s.CanWrite).Returns(true);
|
||||||
|
mock.Setup(s => s.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<byte[], int, int, CancellationToken>((b, o, c, t) =>
|
||||||
|
{
|
||||||
|
written = new ArraySegment<byte>(b, 0, c).ToArray();
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(0));
|
||||||
|
|
||||||
|
var stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(true), null, null);
|
||||||
|
|
||||||
|
stream.DisableResponseBuffering();
|
||||||
|
|
||||||
|
var path = "testfile1kb.txt";
|
||||||
|
await stream.SendFileAsync(path, 0, null, CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Equal(File.ReadAllBytes(path), written);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NET451
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public void BeginWrite_IsPassedToUnderlyingStream_WhenDisableResponseBuffering(bool flushable)
|
||||||
|
{
|
||||||
|
var buffer = new byte[] { 1 };
|
||||||
|
byte[] written = null;
|
||||||
|
|
||||||
|
var mock = new Mock<Stream>();
|
||||||
|
mock.SetupGet(s => s.CanWrite).Returns(true);
|
||||||
|
mock.Setup(s => s.WriteAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<byte[], int, int, CancellationToken>((b, o, c, t) =>
|
||||||
|
{
|
||||||
|
written = new ArraySegment<byte>(b, 0, c).ToArray();
|
||||||
|
})
|
||||||
|
.Returns(Task.FromResult(0));
|
||||||
|
|
||||||
|
var stream = new BodyWrapperStream(new DefaultHttpContext(), mock.Object, new MockResponseCompressionProvider(flushable), null, null);
|
||||||
|
|
||||||
|
stream.DisableResponseBuffering();
|
||||||
|
stream.BeginWrite(buffer, 0, buffer.Length, (o) => {}, null);
|
||||||
|
|
||||||
|
Assert.Equal(buffer, written);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private class MockResponseCompressionProvider: IResponseCompressionProvider
|
||||||
|
{
|
||||||
|
private readonly bool _flushable;
|
||||||
|
|
||||||
|
public MockResponseCompressionProvider(bool flushable)
|
||||||
|
{
|
||||||
|
_flushable = flushable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICompressionProvider GetCompressionProvider(HttpContext context)
|
||||||
|
{
|
||||||
|
return new MockCompressionProvider(_flushable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldCompressResponse(HttpContext context)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CheckRequestAcceptsCompression(HttpContext context)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private class MockCompressionProvider : ICompressionProvider
|
||||||
|
{
|
||||||
|
public MockCompressionProvider(bool flushable)
|
||||||
|
{
|
||||||
|
SupportsFlush = flushable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EncodingName { get; }
|
||||||
|
|
||||||
|
public bool SupportsFlush { get; }
|
||||||
|
|
||||||
|
public Stream CreateStream(Stream outputStream)
|
||||||
|
{
|
||||||
|
if (SupportsFlush)
|
||||||
|
{
|
||||||
|
return new BufferedStream(outputStream);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new NoFlushBufferedStream(outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NoFlushBufferedStream : Stream
|
||||||
|
{
|
||||||
|
private readonly BufferedStream _bufferedStream;
|
||||||
|
|
||||||
|
public NoFlushBufferedStream(Stream outputStream)
|
||||||
|
{
|
||||||
|
_bufferedStream = new BufferedStream(outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count) => _bufferedStream.Read(buffer, offset, count);
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) => _bufferedStream.Seek(offset, origin);
|
||||||
|
|
||||||
|
public override void SetLength(long value) => _bufferedStream.SetLength(value);
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => _bufferedStream.Write(buffer, offset, count);
|
||||||
|
|
||||||
|
public override bool CanRead => _bufferedStream.CanRead;
|
||||||
|
|
||||||
|
public override bool CanSeek => _bufferedStream.CanSeek;
|
||||||
|
|
||||||
|
public override bool CanWrite => _bufferedStream.CanWrite;
|
||||||
|
|
||||||
|
public override long Length => _bufferedStream.Length;
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get { return _bufferedStream.Position; }
|
||||||
|
set { _bufferedStream.Position = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
_bufferedStream.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"buildOptions": {
|
"buildOptions": {
|
||||||
|
"keyFile": "../../tools/Key.snk",
|
||||||
"copyToOutput": [
|
"copyToOutput": [
|
||||||
"testfile1kb.txt"
|
"testfile1kb.txt"
|
||||||
],
|
],
|
||||||
|
@ -11,6 +12,7 @@
|
||||||
"Microsoft.AspNetCore.ResponseCompression": "1.0.0-*",
|
"Microsoft.AspNetCore.ResponseCompression": "1.0.0-*",
|
||||||
"Microsoft.AspNetCore.TestHost": "1.1.0-*",
|
"Microsoft.AspNetCore.TestHost": "1.1.0-*",
|
||||||
"Microsoft.Net.Http.Headers": "1.1.0-*",
|
"Microsoft.Net.Http.Headers": "1.1.0-*",
|
||||||
|
"Moq": "4.6.36-*",
|
||||||
"xunit": "2.2.0-*"
|
"xunit": "2.2.0-*"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче