diff --git a/BasicMiddleware.sln b/BasicMiddleware.sln
index a431cdab..9f345738 100644
--- a/BasicMiddleware.sln
+++ b/BasicMiddleware.sln
@@ -32,6 +32,12 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RewriteSample", "samples\Re
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Rewrite.Tests", "test\Microsoft.AspNetCore.Rewrite.Tests\Microsoft.AspNetCore.Rewrite.Tests.xproj", "{31794F9E-A1AA-4535-B03C-A3233737CD1A}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCompression", "src\Microsoft.AspNetCore.ResponseCompression\Microsoft.AspNetCore.ResponseCompression.xproj", "{45308A9D-F4C6-46A8-A24F-E73D995CC223}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCompression.Tests", "test\Microsoft.AspNetCore.ResponseCompression.Tests\Microsoft.AspNetCore.ResponseCompression.Tests.xproj", "{3360A5D1-70C0-49EE-9051-04A6A6B836DC}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCompressionSample", "samples\ResponseCompressionSample\ResponseCompressionSample.xproj", "{B2A3CE38-51B2-4486-982C-98C380AF140E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -74,6 +80,18 @@ Global
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31794F9E-A1AA-4535-B03C-A3233737CD1A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {45308A9D-F4C6-46A8-A24F-E73D995CC223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {45308A9D-F4C6-46A8-A24F-E73D995CC223}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {45308A9D-F4C6-46A8-A24F-E73D995CC223}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {45308A9D-F4C6-46A8-A24F-E73D995CC223}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3360A5D1-70C0-49EE-9051-04A6A6B836DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B2A3CE38-51B2-4486-982C-98C380AF140E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B2A3CE38-51B2-4486-982C-98C380AF140E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B2A3CE38-51B2-4486-982C-98C380AF140E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B2A3CE38-51B2-4486-982C-98C380AF140E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -88,5 +106,8 @@ Global
{0E7CA1A7-1DC3-4CE6-B9C7-1688FE1410F1} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
{9E049645-13BC-4598-89E1-5B43D36E5D14} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
{31794F9E-A1AA-4535-B03C-A3233737CD1A} = {8437B0F3-3894-4828-A945-A9187F37631D}
+ {45308A9D-F4C6-46A8-A24F-E73D995CC223} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
+ {3360A5D1-70C0-49EE-9051-04A6A6B836DC} = {8437B0F3-3894-4828-A945-A9187F37631D}
+ {B2A3CE38-51B2-4486-982C-98C380AF140E} = {9587FE9F-5A17-42C4-8021-E87F59CECB98}
EndGlobalSection
EndGlobal
diff --git a/samples/ResponseCompressionSample/LoremIpsum.cs b/samples/ResponseCompressionSample/LoremIpsum.cs
new file mode 100644
index 00000000..34eaab21
--- /dev/null
+++ b/samples/ResponseCompressionSample/LoremIpsum.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace ResponseCompressionSample
+{
+ internal static class LoremIpsum
+ {
+ internal const string Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit." +
+ "Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar.Nulla sollicitudin.Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula.Pellentesque rhoncus nunc et augue.Integer id felis.Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas.Lorem ipsum dolor sit amet, consectetuer adipiscing elit.Morbi vel erat non mauris convallis vehicula.Nulla et sapien.Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam.Mauris ullamcorper felis vitae erat.Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna." +
+ "Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien.Vivamus leo. Aliquam euismod libero eu enim.Nulla nec felis sed leo placerat imperdiet.Aenean suscipit nulla in justo.Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.";
+ }
+}
diff --git a/samples/ResponseCompressionSample/Properties/launchSettings.json b/samples/ResponseCompressionSample/Properties/launchSettings.json
new file mode 100644
index 00000000..22f9a791
--- /dev/null
+++ b/samples/ResponseCompressionSample/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:49658/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "web": {
+ "commandName": "web",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/ResponseCompressionSample/ResponseCompressionSample.xproj b/samples/ResponseCompressionSample/ResponseCompressionSample.xproj
new file mode 100644
index 00000000..66a15a5b
--- /dev/null
+++ b/samples/ResponseCompressionSample/ResponseCompressionSample.xproj
@@ -0,0 +1,18 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ b2a3ce38-51b2-4486-982c-98c380af140e
+ .\obj
+ .\bin\
+
+
+ 2.0
+ 46824
+
+
+
\ No newline at end of file
diff --git a/samples/ResponseCompressionSample/Startup.cs b/samples/ResponseCompressionSample/Startup.cs
new file mode 100644
index 00000000..63addfa7
--- /dev/null
+++ b/samples/ResponseCompressionSample/Startup.cs
@@ -0,0 +1,37 @@
+// 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 Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.ResponseCompression;
+
+namespace ResponseCompressionSample
+{
+ public class Startup
+ {
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseResponseCompression(new ResponseCompressionOptions()
+ {
+ ShouldCompressResponse = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(new string[] { "text/plain" })
+ });
+
+ app.Run(async context =>
+ {
+ context.Response.Headers["Content-Type"] = "text/plain";
+ await context.Response.WriteAsync(LoremIpsum.Text);
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/samples/ResponseCompressionSample/project.json b/samples/ResponseCompressionSample/project.json
new file mode 100644
index 00000000..7ad12a13
--- /dev/null
+++ b/samples/ResponseCompressionSample/project.json
@@ -0,0 +1,31 @@
+{
+ "version": "1.1.0-*",
+ "dependencies": {
+ "Microsoft.AspNetCore.ResponseCompression": "0.1.0-*",
+ "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*"
+ },
+ "buildOptions": {
+ "emitEntryPoint": true,
+ "preserveCompilationContext": true
+ },
+ "frameworks": {
+ "net451": {},
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0-*",
+ "type": "platform"
+ }
+ }
+ }
+ },
+ "publish": {
+ "exclude": [
+ "node_modules",
+ "bower_components",
+ "**.xproj",
+ "**.user",
+ "**.vspscc"
+ ]
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs b/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs
new file mode 100644
index 00000000..8862e92d
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs
@@ -0,0 +1,192 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// Stream wrapper that create specific compression stream only if necessary.
+ ///
+ internal class BodyWrapperStream : Stream
+ {
+ private readonly HttpResponse _response;
+
+ private readonly Stream _bodyOriginalStream;
+
+ private readonly Func _shouldCompressResponse;
+
+ private readonly IResponseCompressionProvider _compressionProvider;
+
+ private bool _compressionChecked = false;
+
+ private Stream _compressionStream = null;
+
+ internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, Func shouldCompressResponse, IResponseCompressionProvider compressionProvider)
+ {
+ _response = response;
+ _bodyOriginalStream = bodyOriginalStream;
+ _shouldCompressResponse = shouldCompressResponse;
+ _compressionProvider = compressionProvider;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (_compressionStream != null)
+ {
+ _compressionStream.Dispose();
+ _compressionStream = null;
+ }
+ }
+
+ public override bool CanRead => _bodyOriginalStream.CanRead;
+
+ public override bool CanSeek => _bodyOriginalStream.CanSeek;
+
+ public override bool CanWrite => _bodyOriginalStream.CanWrite;
+
+ public override long Length
+ {
+ get
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public override long Position
+ {
+ get
+ {
+ throw new NotSupportedException();
+ }
+
+ set
+ {
+ throw new NotSupportedException();
+ }
+ }
+
+ public override void Flush()
+ {
+ OnWrite();
+
+ if (_compressionStream != null)
+ {
+ _compressionStream.Flush();
+ }
+ else
+ {
+ _bodyOriginalStream.Flush();
+ }
+ }
+
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ OnWrite();
+
+ if (_compressionStream != null)
+ {
+ return _compressionStream.FlushAsync(cancellationToken);
+ }
+ return _bodyOriginalStream.FlushAsync(cancellationToken);
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ OnWrite();
+
+ if (_compressionStream != null)
+ {
+ _compressionStream.Write(buffer, offset, count);
+ }
+ else
+ {
+ _bodyOriginalStream.Write(buffer, offset, count);
+ }
+ }
+
+#if NET451
+ public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
+ {
+ OnWrite();
+
+ if (_compressionStream != null)
+ {
+ return _compressionStream.BeginWrite(buffer, offset, count, callback, state);
+ }
+ return _bodyOriginalStream.BeginWrite(buffer, offset, count, callback, state);
+ }
+
+ public override void EndWrite(IAsyncResult asyncResult)
+ {
+ OnWrite();
+
+ if (_compressionStream != null)
+ {
+ _compressionStream.EndWrite(asyncResult);
+ }
+ else
+ {
+ _bodyOriginalStream.EndWrite(asyncResult);
+ }
+ }
+#endif
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ OnWrite();
+
+ if (_compressionStream != null)
+ {
+ return _compressionStream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+ return _bodyOriginalStream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+
+ private void OnWrite()
+ {
+ if (!_compressionChecked)
+ {
+ if (IsCompressable())
+ {
+ _response.Headers[HeaderNames.ContentEncoding] = _compressionProvider.EncodingName;
+ _response.Headers.Remove(HeaderNames.ContentMD5); // Reset the MD5 because the content changed.
+ _response.Headers.Remove(HeaderNames.ContentLength);
+
+ _compressionStream = _compressionProvider.CreateStream(_bodyOriginalStream);
+ }
+
+ _compressionChecked = true;
+ }
+ }
+
+ private bool IsCompressable()
+ {
+ return _response.Headers[HeaderNames.ContentRange] == StringValues.Empty && // The response is not partial
+ _response.Headers[HeaderNames.ContentEncoding] == StringValues.Empty && // Not specific encoding already set
+ _shouldCompressResponse(_response.HttpContext);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/GzipResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/GzipResponseCompressionProvider.cs
new file mode 100644
index 00000000..9496f951
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/GzipResponseCompressionProvider.cs
@@ -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.IO;
+using System.IO.Compression;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// GZIP compression provider.
+ ///
+ public class GzipResponseCompressionProvider : IResponseCompressionProvider
+ {
+ private readonly CompressionLevel _level;
+
+ ///
+ /// Initialize a new .
+ ///
+ /// The compression level.
+ public GzipResponseCompressionProvider(CompressionLevel level)
+ {
+ _level = level;
+ }
+
+ ///
+ public string EncodingName { get; } = "gzip";
+
+ ///
+ public Stream CreateStream(Stream outputStream)
+ {
+ return new GZipStream(outputStream, _level, true);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs
new file mode 100644
index 00000000..b91ed1db
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs
@@ -0,0 +1,25 @@
+// 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;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// Provides methods to be able to compress HTTP responses.
+ ///
+ public interface IResponseCompressionProvider
+ {
+ ///
+ /// The name that will be searched in the 'Accept-Encoding' request header.
+ ///
+ string EncodingName { get; }
+
+ ///
+ /// Create a new compression stream.
+ ///
+ /// The stream where the compressed data have to be written.
+ /// The new stream.
+ Stream CreateStream(Stream outputStream);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.xproj b/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.xproj
new file mode 100644
index 00000000..0bd5bdaa
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.xproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 45308a9d-f4c6-46a8-a24f-e73d995cc223
+ .\obj
+ .\bin\
+
+
+ 2.0
+
+
+
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.ResponseCompression/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..2dc4003a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/Properties/AssemblyInfo.cs
@@ -0,0 +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.Reflection;
+using System.Resources;
+
+[assembly: AssemblyMetadata("Serviceable", "True")]
+[assembly: NeutralResourcesLanguage("en-us")]
+[assembly: AssemblyCompany("Microsoft Corporation.")]
+[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
+[assembly: AssemblyProduct("Microsoft ASP.NET Core")]
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionExtensions.cs
new file mode 100644
index 00000000..720d9c30
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionExtensions.cs
@@ -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;
+using Microsoft.AspNetCore.ResponseCompression;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ ///
+ /// Extension methods for the ResponseCompression middleware.
+ ///
+ public static class ResponseCompressionExtensions
+ {
+ ///
+ /// Allows to compress HTTP Responses.
+ ///
+ /// The instance this method extends.
+ /// The .
+ public static IApplicationBuilder UseResponseCompression(this IApplicationBuilder builder, ResponseCompressionOptions options)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ return builder.UseMiddleware(Options.Create(options));
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs
new file mode 100644
index 00000000..4d9a0e61
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs
@@ -0,0 +1,125 @@
+// 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.Collections.Generic;
+using System.IO.Compression;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// Enable HTTP response compression.
+ ///
+ public class ResponseCompressionMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ private readonly Dictionary _compressionProviders;
+
+ private readonly Func _shouldCompressResponse;
+
+ private readonly bool _enableHttps;
+
+ ///
+ /// Initialize the Response Compression middleware.
+ ///
+ ///
+ ///
+ public ResponseCompressionMiddleware(RequestDelegate next, IOptions options)
+ {
+ if (options.Value.ShouldCompressResponse == null)
+ {
+ throw new ArgumentException($"{nameof(options.Value.ShouldCompressResponse)} is not provided in argument {nameof(options)}");
+ }
+ _shouldCompressResponse = options.Value.ShouldCompressResponse;
+
+ _next = next;
+
+ var providers = options.Value.Providers;
+ if (providers == null)
+ {
+ providers = new IResponseCompressionProvider[]
+ {
+ new GzipResponseCompressionProvider(CompressionLevel.Fastest)
+ };
+ }
+ else if (!providers.Any())
+ {
+ throw new ArgumentException($"{nameof(options.Value.Providers)} cannot be empty in argument {nameof(options)}");
+ }
+
+ _compressionProviders = providers.ToDictionary(p => p.EncodingName, StringComparer.OrdinalIgnoreCase);
+ _compressionProviders.Add("*", providers.First());
+ _compressionProviders.Add("identity", null);
+
+ _enableHttps = options.Value.EnableHttps;
+ }
+
+ ///
+ /// Invoke the middleware.
+ ///
+ ///
+ ///
+ public async Task Invoke(HttpContext context)
+ {
+ IResponseCompressionProvider compressionProvider = null;
+
+ if (!context.Request.IsHttps || _enableHttps)
+ {
+ compressionProvider = SelectProvider(context.Request.Headers[HeaderNames.AcceptEncoding]);
+ }
+
+ if (compressionProvider == null)
+ {
+ await _next(context);
+ return;
+ }
+
+ var bodyStream = context.Response.Body;
+
+ using (var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _shouldCompressResponse, compressionProvider))
+ {
+ context.Response.Body = bodyWrapperStream;
+
+ try
+ {
+ await _next(context);
+ }
+ finally
+ {
+ context.Response.Body = bodyStream;
+ }
+ }
+ }
+
+ private IResponseCompressionProvider SelectProvider(StringValues acceptEncoding)
+ {
+ IList unsorted;
+
+ if (StringWithQualityHeaderValue.TryParseList(acceptEncoding, out unsorted) && unsorted != null)
+ {
+ var sorted = unsorted
+ .Where(s => s.Quality.GetValueOrDefault(1) > 0)
+ .OrderByDescending(s => s.Quality.GetValueOrDefault(1));
+
+ foreach (var encoding in sorted)
+ {
+ IResponseCompressionProvider provider;
+
+ if (_compressionProviders.TryGetValue(encoding.Value, out provider))
+ {
+ return provider;
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs
new file mode 100644
index 00000000..b3548bbb
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs
@@ -0,0 +1,32 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// Options for the HTTP response compression middleware.
+ ///
+ public class ResponseCompressionOptions
+ {
+ ///
+ /// Called when an HTTP request accepts a compatible compression algorithm, and returns True
+ /// if the response should be compressed.
+ ///
+ public Func ShouldCompressResponse { get; set; }
+
+ ///
+ /// The compression providers. If 'null', the GZIP provider is set as default.
+ ///
+ public IEnumerable Providers { get; set; }
+
+ ///
+ /// 'False' to enable compression only on HTTP requests. Enable compression on HTTPS requests
+ /// may lead to security problems.
+ ///
+ public bool EnableHttps { get; set; } = false;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionUtils.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionUtils.cs
new file mode 100644
index 00000000..525dc33a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionUtils.cs
@@ -0,0 +1,49 @@
+// 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.Collections.Generic;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// Response Compression middleware utility methods.
+ ///
+ public static class ResponseCompressionUtils
+ {
+ ///
+ /// Create a delegate that propose to compress response, depending on a list of authorized
+ /// MIME types for the HTTP response.
+ ///
+ public static Func CreateShouldCompressResponseDelegate(IEnumerable mimeTypes)
+ {
+ if (mimeTypes == null)
+ {
+ throw new ArgumentNullException(nameof(mimeTypes));
+ }
+
+ var mimeTypeSet = new HashSet(mimeTypes);
+
+ return (httpContext) =>
+ {
+ var mimeType = httpContext.Response.ContentType;
+
+ if (string.IsNullOrEmpty(mimeType))
+ {
+ return false;
+ }
+
+ var separator = mimeType.IndexOf(';');
+ if (separator >= 0)
+ {
+ // Remove the content-type optional parameters
+ mimeType = mimeType.Substring(0, separator);
+ mimeType = mimeType.Trim();
+ }
+
+ return mimeTypeSet.Contains(mimeType);
+ };
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/project.json b/src/Microsoft.AspNetCore.ResponseCompression/project.json
new file mode 100644
index 00000000..effe3df4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/project.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.1.0-*",
+ "buildOptions": {
+ "warningsAsErrors": true,
+ "keyFile": "../../tools/Key.snk",
+ "xmlDoc": true
+ },
+ "description": "ASP.NET Core middleware for HTTP Response compression.",
+ "packOptions": {
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/aspnet/basicmiddleware"
+ },
+ "tags": [
+ "aspnetcore"
+ ]
+ },
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Abstractions": "1.1.0-*",
+ "Microsoft.Extensions.Options": "1.1.0-*",
+ "Microsoft.Net.Http.Headers": "1.1.0-*"
+ },
+ "frameworks": {
+ "net451": {},
+ "netstandard1.3": {
+ "dependencies": {
+ "System.IO.Compression": "4.1.0-*"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/Microsoft.AspNetCore.ResponseCompression.Tests.xproj b/test/Microsoft.AspNetCore.ResponseCompression.Tests/Microsoft.AspNetCore.ResponseCompression.Tests.xproj
new file mode 100644
index 00000000..756da5af
--- /dev/null
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/Microsoft.AspNetCore.ResponseCompression.Tests.xproj
@@ -0,0 +1,20 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 3360a5d1-70c0-49ee-9051-04a6a6b836dc
+ .\obj
+ .\bin\
+
+
+ 2.0
+
+
+
+
+
+
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
new file mode 100644
index 00000000..76cdcc9f
--- /dev/null
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
@@ -0,0 +1,242 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCompression.Tests
+{
+ public class ResponseCompressionMiddlewareTest
+ {
+ private const string TextPlain = "text/plain";
+
+ [Fact]
+ public void Options_NullShouldCompressResponse()
+ {
+ Assert.Throws(() =>
+ {
+ new ResponseCompressionMiddleware(null, Options.Create(new ResponseCompressionOptions()
+ {
+ ShouldCompressResponse = null
+ }));
+ });
+ }
+
+ [Fact]
+ public void Options_HttpsDisabledByDefault()
+ {
+ var options = new ResponseCompressionOptions();
+
+ Assert.False(options.EnableHttps);
+ }
+
+ [Fact]
+ public void Options_EmptyProviderList()
+ {
+ Assert.Throws(() =>
+ {
+ new ResponseCompressionMiddleware(null, Options.Create(new ResponseCompressionOptions()
+ {
+ ShouldCompressResponse = _ => true,
+ Providers = new IResponseCompressionProvider[0]
+ }));
+ });
+ }
+
+ [Fact]
+ public async Task Request_Uncompressed()
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: null, responseType: TextPlain);
+
+ CheckResponseNotCompressed(response, expectedBodyLength: 100);
+ }
+
+ [Fact]
+ public async Task Request_CompressGzip()
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "gzip", "deflate" }, responseType: TextPlain);
+
+ CheckResponseCompressed(response, expectedBodyLength: 24);
+ }
+
+ [Fact]
+ public async Task Request_CompressUnknown()
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "unknown" }, responseType: TextPlain);
+
+ CheckResponseNotCompressed(response, expectedBodyLength: 100);
+ }
+
+ [Fact]
+ public async Task Request_CompressStar()
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "*" }, responseType: TextPlain);
+
+ CheckResponseCompressed(response, expectedBodyLength: 24);
+ }
+
+ [Fact]
+ public async Task Request_CompressIdentity()
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "identity" }, responseType: TextPlain);
+
+ CheckResponseNotCompressed(response, expectedBodyLength: 100);
+ }
+
+ [Theory]
+ [InlineData(new string[] { "identity;q=0.5", "gzip;q=1" }, 24)]
+ [InlineData(new string[] { "identity;q=0", "gzip;q=0.8" }, 24)]
+ [InlineData(new string[] { "identity;q=0.5", "gzip" }, 24)]
+ public async Task Request_CompressQuality_Compressed(string[] acceptEncodings, int expectedBodyLength)
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
+
+ CheckResponseCompressed(response, expectedBodyLength: expectedBodyLength);
+ }
+
+ [Theory]
+ [InlineData(new string[] { "gzip;q=0.5", "identity;q=0.8" }, 100)]
+ public async Task Request_CompressQuality_NotCompressed(string[] acceptEncodings, int expectedBodyLength)
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: acceptEncodings, responseType: TextPlain);
+
+ CheckResponseNotCompressed(response, expectedBodyLength: expectedBodyLength);
+ }
+
+ [Fact]
+ public async Task Request_UnauthorizedMimeType()
+ {
+ var response = await InvokeMiddleware(100, requestAcceptEncodings: new string[] { "gzip" }, responseType: "text/html");
+
+ CheckResponseNotCompressed(response, expectedBodyLength: 100);
+ }
+
+ [Fact]
+ public async Task Request_ResponseWithContentRange()
+ {
+ var response = await InvokeMiddleware(50, requestAcceptEncodings: new string[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
+ {
+ r.Headers[HeaderNames.ContentRange] = "1-2/*";
+ });
+
+ CheckResponseNotCompressed(response, expectedBodyLength: 50);
+ }
+
+ [Fact]
+ public async Task Request_ResponseWithContentEncodingAlreadySet()
+ {
+ var otherContentEncoding = "something";
+
+ var response = await InvokeMiddleware(50, requestAcceptEncodings: new string[] { "gzip" }, responseType: TextPlain, addResponseAction: (r) =>
+ {
+ r.Headers[HeaderNames.ContentEncoding] = otherContentEncoding;
+ });
+
+ Assert.NotNull(response.Headers.GetValues(HeaderNames.ContentMD5));
+ Assert.Single(response.Content.Headers.ContentEncoding, otherContentEncoding);
+ Assert.Equal(50, response.Content.Headers.ContentLength);
+ }
+
+ [Theory]
+ [InlineData(false, 100)]
+ [InlineData(true, 24)]
+ public async Task Request_Https(bool enableHttps, int expectedLength)
+ {
+ var options = new ResponseCompressionOptions()
+ {
+ ShouldCompressResponse = _ => true,
+ Providers = new IResponseCompressionProvider[]
+ {
+ new GzipResponseCompressionProvider(CompressionLevel.Optimal)
+ },
+ EnableHttps = enableHttps
+ };
+
+ var middleware = new ResponseCompressionMiddleware(async context =>
+ {
+ context.Response.ContentType = TextPlain;
+ await context.Response.WriteAsync(new string('a', 100));
+ }, Options.Create(options));
+
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Headers[HeaderNames.AcceptEncoding] = "gzip";
+ httpContext.Request.IsHttps = true;
+
+ httpContext.Response.Body = new MemoryStream();
+
+ await middleware.Invoke(httpContext);
+
+ Assert.Equal(expectedLength, httpContext.Response.Body.Length);
+ }
+
+ private Task InvokeMiddleware(int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action addResponseAction = null)
+ {
+ var options = new ResponseCompressionOptions()
+ {
+ ShouldCompressResponse = ctx =>
+ {
+ var contentType = ctx.Response.Headers[HeaderNames.ContentType];
+ return contentType.ToString().IndexOf(TextPlain) >= 0;
+ },
+ Providers = new IResponseCompressionProvider[]
+ {
+ new GzipResponseCompressionProvider(CompressionLevel.Optimal)
+ }
+ };
+
+ var builder = new WebHostBuilder()
+ .Configure(app =>
+ {
+ app.UseResponseCompression(options);
+ app.Run(context =>
+ {
+ context.Response.Headers[HeaderNames.ContentMD5] = "MD5";
+ context.Response.ContentType = responseType;
+ if (addResponseAction != null)
+ {
+ addResponseAction(context.Response);
+ }
+ return context.Response.WriteAsync(new string('a', uncompressedBodyLength));
+ });
+ });
+
+ var server = new TestServer(builder);
+ var client = server.CreateClient();
+
+ var request = new HttpRequestMessage(HttpMethod.Get, "");
+ for (var i = 0; i < requestAcceptEncodings?.Length; i++)
+ {
+ request.Headers.AcceptEncoding.Add(System.Net.Http.Headers.StringWithQualityHeaderValue.Parse(requestAcceptEncodings[i]));
+ }
+
+ return client.SendAsync(request);
+ }
+
+ private void CheckResponseCompressed(HttpResponseMessage response, int expectedBodyLength)
+ {
+ IEnumerable contentMD5 = null;
+
+ Assert.False(response.Headers.TryGetValues(HeaderNames.ContentMD5, out contentMD5));
+ Assert.Single(response.Content.Headers.ContentEncoding, "gzip");
+ Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
+ }
+
+ private void CheckResponseNotCompressed(HttpResponseMessage response, int expectedBodyLength)
+ {
+ Assert.NotNull(response.Headers.GetValues(HeaderNames.ContentMD5));
+ Assert.Empty(response.Content.Headers.ContentEncoding);
+ Assert.Equal(expectedBodyLength, response.Content.Headers.ContentLength);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionUtilsTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionUtilsTest.cs
new file mode 100644
index 00000000..9efcc79c
--- /dev/null
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionUtilsTest.cs
@@ -0,0 +1,69 @@
+// 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.Linq;
+using Microsoft.AspNetCore.Http;
+using Xunit;
+
+namespace Microsoft.AspNetCore.ResponseCompression.Tests
+{
+ public class ResponseCompressionUtilsTest
+ {
+ private const string TextPlain = "text/plain";
+
+ [Fact]
+ public void CreateShouldCompressResponseDelegate_NullMimeTypes()
+ {
+ Assert.Throws(() =>
+ {
+ ResponseCompressionUtils.CreateShouldCompressResponseDelegate(null);
+ });
+ }
+
+ [Fact]
+ public void CreateShouldCompressResponseDelegate_Empty()
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.ContentType = TextPlain;
+
+ var func = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(Enumerable.Empty());
+
+ var result = func(httpContext);
+
+ Assert.False(result);
+ }
+
+ [Theory]
+ [InlineData("text/plain")]
+ [InlineData("text/plain; charset=ISO-8859-4")]
+ [InlineData("text/plain ; charset=ISO-8859-4")]
+ public void CreateShouldCompressResponseDelegate_True(string contentType)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.ContentType = contentType;
+
+ var func = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(new string[] { TextPlain });
+
+ var result = func(httpContext);
+
+ Assert.True(result);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData("text/plain2")]
+ [InlineData("text/PLAIN")]
+ public void CreateShouldCompressResponseDelegate_False(string contentType)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.ContentType = contentType;
+
+ var func = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(new string[] { TextPlain });
+
+ var result = func(httpContext);
+
+ Assert.False(result);
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCompression.Tests/project.json
new file mode 100644
index 00000000..f9532655
--- /dev/null
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/project.json
@@ -0,0 +1,26 @@
+{
+ "version": "1.1.0-*",
+ "buildOptions": {
+ "warningsAsErrors": true
+ },
+ "dependencies": {
+ "dotnet-test-xunit": "2.2.0-*",
+ "Microsoft.AspNetCore.ResponseCompression": "0.1.0-*",
+ "Microsoft.AspNetCore.TestHost": "1.1.0-*",
+ "xunit": "2.2.0-*",
+ "Microsoft.AspNetCore.Http": "1.1.0-*",
+ "Microsoft.Net.Http.Headers": "1.1.0-*"
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0-*",
+ "type": "platform"
+ }
+ }
+ },
+ "net451": {}
+ },
+ "testRunner": "xunit"
+}
\ No newline at end of file