diff --git a/samples/ResponseCompressionSample/Startup.cs b/samples/ResponseCompressionSample/Startup.cs
index 98af8aea6b..17dd97bd39 100644
--- a/samples/ResponseCompressionSample/Startup.cs
+++ b/samples/ResponseCompressionSample/Startup.cs
@@ -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.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -26,6 +27,15 @@ namespace ResponseCompressionSample
{
app.UseResponseCompression();
+ app.Map("/testfile1kb.txt", fileApp =>
+ {
+ fileApp.Run(context =>
+ {
+ context.Response.ContentType = "text/plain";
+ return context.Response.SendFileAsync("testfile1kb.txt");
+ });
+ });
+
app.Map("/trickle", trickleApp =>
{
trickleApp.Run(async context =>
@@ -57,6 +67,7 @@ namespace ResponseCompressionSample
{
options.UseConnectionLogging();
})
+ // .UseWebListener()
.ConfigureLogging(factory =>
{
factory.AddConsole(LogLevel.Debug);
diff --git a/samples/ResponseCompressionSample/project.json b/samples/ResponseCompressionSample/project.json
index fb7bb30810..07bfa7363d 100644
--- a/samples/ResponseCompressionSample/project.json
+++ b/samples/ResponseCompressionSample/project.json
@@ -2,9 +2,13 @@
"dependencies": {
"Microsoft.AspNetCore.ResponseCompression": "0.1.0-*",
"Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*",
+ "Microsoft.AspNetCore.Server.WebListener": "1.1.0-*",
"Microsoft.Extensions.Logging.Console": "1.1.0-*"
},
"buildOptions": {
+ "copyToOutput": [
+ "testfile1kb.txt"
+ ],
"emitEntryPoint": true
},
"frameworks": {
diff --git a/samples/ResponseCompressionSample/testfile1kb.txt b/samples/ResponseCompressionSample/testfile1kb.txt
new file mode 100644
index 0000000000..24baa4c608
--- /dev/null
+++ b/samples/ResponseCompressionSample/testfile1kb.txt
@@ -0,0 +1 @@
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs b/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs
index 3b31b1f357..1b82521668 100644
--- a/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs
+++ b/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs
@@ -6,6 +6,7 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Net.Http.Headers;
@@ -14,30 +15,27 @@ namespace Microsoft.AspNetCore.ResponseCompression
///
/// Stream wrapper that create specific compression stream only if necessary.
///
- internal class BodyWrapperStream : Stream, IHttpBufferingFeature
+ internal class BodyWrapperStream : Stream, IHttpBufferingFeature, IHttpSendFileFeature
{
private readonly HttpResponse _response;
-
private readonly Stream _bodyOriginalStream;
-
private readonly IResponseCompressionProvider _provider;
-
private readonly ICompressionProvider _compressionProvider;
-
private readonly IHttpBufferingFeature _innerBufferFeature;
+ private readonly IHttpSendFileFeature _innerSendFileFeature;
private bool _compressionChecked = false;
-
private Stream _compressionStream = null;
internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, IResponseCompressionProvider provider, ICompressionProvider compressionProvider,
- IHttpBufferingFeature innerBufferFeature)
+ IHttpBufferingFeature innerBufferFeature, IHttpSendFileFeature innerSendFileFeature)
{
_response = response;
_bodyOriginalStream = bodyOriginalStream;
_provider = provider;
_compressionProvider = compressionProvider;
_innerBufferFeature = innerBufferFeature;
+ _innerSendFileFeature = innerSendFileFeature;
}
protected override void Dispose(bool disposing)
@@ -216,5 +214,50 @@ namespace Microsoft.AspNetCore.ResponseCompression
_innerBufferFeature?.DisableResponseBuffering();
}
+
+ // 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 _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);
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs
index 6e2e784bc5..692d77f643 100644
--- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs
+++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs
@@ -68,10 +68,16 @@ namespace Microsoft.AspNetCore.ResponseCompression
var bodyStream = context.Response.Body;
var originalBufferFeature = context.Features.Get();
+ var originalSendFileFeature = context.Features.Get();
- var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _provider, compressionProvider, originalBufferFeature);
+ var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _provider, compressionProvider,
+ originalBufferFeature, originalSendFileFeature);
context.Response.Body = bodyWrapperStream;
context.Features.Set(bodyWrapperStream);
+ if (originalSendFileFeature != null)
+ {
+ context.Features.Set(bodyWrapperStream);
+ }
try
{
@@ -84,6 +90,10 @@ namespace Microsoft.AspNetCore.ResponseCompression
{
context.Response.Body = bodyStream;
context.Features.Set(originalBufferFeature);
+ if (originalSendFileFeature != null)
+ {
+ context.Features.Set(originalSendFileFeature);
+ }
}
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/project.json b/src/Microsoft.AspNetCore.ResponseCompression/project.json
index 3cea610ec4..cc3a281c15 100644
--- a/src/Microsoft.AspNetCore.ResponseCompression/project.json
+++ b/src/Microsoft.AspNetCore.ResponseCompression/project.json
@@ -16,9 +16,8 @@
]
},
"dependencies": {
- "Microsoft.AspNetCore.Http.Abstractions": "1.1.0-*",
+ "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*",
"Microsoft.Extensions.Options": "1.1.0-*",
- "Microsoft.Net.Http.Headers": "1.1.0-*",
"NETStandard.Library": "1.6.1-*"
},
"frameworks": {
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
index f76f56d704..8f468d8b03 100644
--- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -579,6 +580,168 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
}
}
+ [Fact]
+ public async Task SendFileAsync_OnlySetIfFeatureAlreadyExists()
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddResponseCompression(TextPlain);
+ })
+ .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();
+ 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()
+ {
+ FakeSendFileFeature fakeSendFile = null;
+
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddResponseCompression(TextPlain);
+ })
+ .Configure(app =>
+ {
+ app.Use((context, next) =>
+ {
+ fakeSendFile = new FakeSendFileFeature(context.Response.Body);
+ context.Features.Set(fakeSendFile);
+ return next();
+ });
+ app.UseResponseCompression();
+ app.Run(context =>
+ {
+ context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
+ context.Response.ContentType = "custom/type";
+ context.Response.ContentLength = 1024;
+ var sendFile = context.Features.Get();
+ Assert.NotNull(sendFile);
+ return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
+ });
+ });
+
+ 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);
+
+ CheckResponseNotCompressed(response, expectedBodyLength: 1024);
+
+ Assert.True(fakeSendFile.Invoked);
+ }
+
+ [Fact]
+ public async Task SendFileAsync_FirstWrite_CompressesAndFlushes()
+ {
+ FakeSendFileFeature fakeSendFile = null;
+
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddResponseCompression(TextPlain);
+ })
+ .Configure(app =>
+ {
+ app.Use((context, next) =>
+ {
+ fakeSendFile = new FakeSendFileFeature(context.Response.Body);
+ context.Features.Set(fakeSendFile);
+ return next();
+ });
+ app.UseResponseCompression();
+ app.Run(context =>
+ {
+ context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
+ context.Response.ContentType = TextPlain;
+ context.Response.ContentLength = 1024;
+ var sendFile = context.Features.Get();
+ Assert.NotNull(sendFile);
+ return sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
+ });
+ });
+
+ 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);
+
+ CheckResponseCompressed(response, expectedBodyLength: 34);
+
+ Assert.False(fakeSendFile.Invoked);
+ }
+
+ [Fact]
+ public async Task SendFileAsync_AfterFirstWrite_CompressesAndFlushes()
+ {
+ FakeSendFileFeature fakeSendFile = null;
+
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddResponseCompression(TextPlain);
+ })
+ .Configure(app =>
+ {
+ app.Use((context, next) =>
+ {
+ fakeSendFile = new FakeSendFileFeature(context.Response.Body);
+ context.Features.Set(fakeSendFile);
+ return next();
+ });
+ app.UseResponseCompression();
+ app.Run(async context =>
+ {
+ context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
+ context.Response.ContentType = TextPlain;
+ var sendFile = context.Features.Get();
+ Assert.NotNull(sendFile);
+
+ await context.Response.WriteAsync(new string('a', 100));
+ await sendFile.SendFileAsync("testfile1kb.txt", 0, null, CancellationToken.None);
+ });
+ });
+
+ 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);
+
+ CheckResponseCompressed(response, expectedBodyLength: 40);
+
+ Assert.False(fakeSendFile.Invoked);
+ }
+
private Task InvokeMiddleware(int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action addResponseAction = null)
{
var builder = new WebHostBuilder()
@@ -593,6 +756,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
context.Response.ContentType = responseType;
+ Assert.Null(context.Features.Get());
if (addResponseAction != null)
{
addResponseAction(context.Response);
@@ -628,5 +792,32 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
Assert.Empty(response.Content.Headers.ContentEncoding);
Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
}
+
+ private class FakeSendFileFeature : IHttpSendFileFeature
+ {
+ private readonly Stream _innerBody;
+
+ public FakeSendFileFeature(Stream innerBody)
+ {
+ _innerBody = innerBody;
+ }
+
+ public bool Invoked { get; set; }
+
+ public async 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);
+ }
+ }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCompression.Tests/project.json
index 26238aab92..af9226646a 100644
--- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/project.json
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/project.json
@@ -1,6 +1,9 @@
{
"version": "1.1.0-*",
"buildOptions": {
+ "copyToOutput": [
+ "testfile1kb.txt"
+ ],
"warningsAsErrors": true
},
"dependencies": {
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/testfile1kb.txt b/test/Microsoft.AspNetCore.ResponseCompression.Tests/testfile1kb.txt
new file mode 100644
index 0000000000..24baa4c608
--- /dev/null
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/testfile1kb.txt
@@ -0,0 +1 @@
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
\ No newline at end of file