From 9bc8a83a392545bdca11d47383f0f3ba0e7a2a28 Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 30 Sep 2016 10:15:46 -0700 Subject: [PATCH] #108 Change ResponseCompression to be DI centric --- .../CustomCompressionProvider.cs | 16 ++ .../Properties/launchSettings.json | 19 +- samples/ResponseCompressionSample/Startup.cs | 15 +- .../BodyWrapperStream.cs | 15 +- ...Provider.cs => GzipCompressionProvider.cs} | 19 +- .../ICompressionProvider.cs | 25 +++ .../IResponseCompressionProvider.cs | 18 +- .../ResponseCompressionExtensions.cs | 50 ++++- .../ResponseCompressionMiddleware.cs | 76 ++------ .../ResponseCompressionOptions.cs | 16 +- .../ResponseCompressionProvider.cs | 114 +++++++++++ .../ResponseCompressionUtils.cs | 49 ----- .../ResponseCompressionMiddlewareTest.cs | 184 +++++++++++------- .../ResponseCompressionUtilsTest.cs | 69 ------- 14 files changed, 374 insertions(+), 311 deletions(-) create mode 100644 samples/ResponseCompressionSample/CustomCompressionProvider.cs rename src/Microsoft.AspNetCore.ResponseCompression/{GzipResponseCompressionProvider.cs => GzipCompressionProvider.cs} (53%) create mode 100644 src/Microsoft.AspNetCore.ResponseCompression/ICompressionProvider.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionUtils.cs delete mode 100644 test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionUtilsTest.cs diff --git a/samples/ResponseCompressionSample/CustomCompressionProvider.cs b/samples/ResponseCompressionSample/CustomCompressionProvider.cs new file mode 100644 index 0000000000..f9d4781bbf --- /dev/null +++ b/samples/ResponseCompressionSample/CustomCompressionProvider.cs @@ -0,0 +1,16 @@ +using System.IO; +using Microsoft.AspNetCore.ResponseCompression; + +namespace ResponseCompressionSample +{ + public class CustomCompressionProvider : ICompressionProvider + { + public string EncodingName => "custom"; + + public Stream CreateStream(Stream outputStream) + { + // Create a custom compression stream wrapper here + return outputStream; + } + } +} diff --git a/samples/ResponseCompressionSample/Properties/launchSettings.json b/samples/ResponseCompressionSample/Properties/launchSettings.json index 22f9a79116..f6c233feed 100644 --- a/samples/ResponseCompressionSample/Properties/launchSettings.json +++ b/samples/ResponseCompressionSample/Properties/launchSettings.json @@ -1,22 +1,9 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:49658/", - "sslPort": 0 - } - }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "ResponseCompressionSample": { + "commandName": "Project", "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "web": { - "commandName": "web", + "launchUrl": "http://localhost:5000/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/samples/ResponseCompressionSample/Startup.cs b/samples/ResponseCompressionSample/Startup.cs index 63addfa7eb..f5fdd6126c 100644 --- a/samples/ResponseCompressionSample/Startup.cs +++ b/samples/ResponseCompressionSample/Startup.cs @@ -5,21 +5,26 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.DependencyInjection; namespace ResponseCompressionSample { public class Startup { + public void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddResponseCompression("text/plain", "text/html"); + } + public void Configure(IApplicationBuilder app) { - app.UseResponseCompression(new ResponseCompressionOptions() - { - ShouldCompressResponse = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(new string[] { "text/plain" }) - }); + app.UseResponseCompression(); app.Run(async context => { - context.Response.Headers["Content-Type"] = "text/plain"; + context.Response.ContentType = "text/plain"; await context.Response.WriteAsync(LoremIpsum.Text); }); } diff --git a/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs b/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs index 50e8ede353..453ede8899 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/BodyWrapperStream.cs @@ -2,12 +2,10 @@ // 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 @@ -21,19 +19,19 @@ namespace Microsoft.AspNetCore.ResponseCompression private readonly Stream _bodyOriginalStream; - private readonly Func _shouldCompressResponse; + private readonly IResponseCompressionProvider _provider; - private readonly IResponseCompressionProvider _compressionProvider; + private readonly ICompressionProvider _compressionProvider; private bool _compressionChecked = false; private Stream _compressionStream = null; - internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, Func shouldCompressResponse, IResponseCompressionProvider compressionProvider) + internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, IResponseCompressionProvider provider, ICompressionProvider compressionProvider) { _response = response; _bodyOriginalStream = bodyOriginalStream; - _shouldCompressResponse = shouldCompressResponse; + _provider = provider; _compressionProvider = compressionProvider; } @@ -174,9 +172,8 @@ namespace Microsoft.AspNetCore.ResponseCompression 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); + return !_response.Headers.ContainsKey(HeaderNames.ContentRange) && // The response is not partial + _provider.ShouldCompressResponse(_response.HttpContext); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCompression/GzipResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/GzipCompressionProvider.cs similarity index 53% rename from src/Microsoft.AspNetCore.ResponseCompression/GzipResponseCompressionProvider.cs rename to src/Microsoft.AspNetCore.ResponseCompression/GzipCompressionProvider.cs index 9496f95197..ff2c20c9b7 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/GzipResponseCompressionProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/GzipCompressionProvider.cs @@ -9,26 +9,27 @@ namespace Microsoft.AspNetCore.ResponseCompression /// /// GZIP compression provider. /// - public class GzipResponseCompressionProvider : IResponseCompressionProvider + public class GzipCompressionProvider : ICompressionProvider { - private readonly CompressionLevel _level; - /// - /// Initialize a new . + /// Initialize a new . /// - /// The compression level. - public GzipResponseCompressionProvider(CompressionLevel level) + public GzipCompressionProvider() { - _level = level; } /// - public string EncodingName { get; } = "gzip"; + public string EncodingName => "gzip"; + + /// + /// What level of compression to use for the stream. + /// + public CompressionLevel Level { get; set; } = CompressionLevel.Fastest; /// public Stream CreateStream(Stream outputStream) { - return new GZipStream(outputStream, _level, true); + return new GZipStream(outputStream, Level, leaveOpen: true); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ICompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/ICompressionProvider.cs new file mode 100644 index 0000000000..66ae5cca51 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCompression/ICompressionProvider.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 a specific compression implementation to compress HTTP responses. + /// + public interface ICompressionProvider + { + /// + /// The encoding name used in the 'Accept-Encoding' request header and 'Content-Encoding' response header. + /// + string EncodingName { get; } + + /// + /// Create a new compression stream. + /// + /// The stream where the compressed data have to be written. + /// The compression stream. + Stream CreateStream(Stream outputStream); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs index b91ed1db8d..8b118a2d89 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/IResponseCompressionProvider.cs @@ -1,25 +1,27 @@ // 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 Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCompression { /// - /// Provides methods to be able to compress HTTP responses. + /// Used to examine requests and responses to see if compression should be enabled. /// public interface IResponseCompressionProvider { /// - /// The name that will be searched in the 'Accept-Encoding' request header. + /// Examines the request and selects an acceptable compression provider, if any. /// - string EncodingName { get; } + /// + /// A compression provider or null if compression should not be used. + ICompressionProvider GetCompressionProvider(HttpContext context); /// - /// Create a new compression stream. + /// Examines the response on first write to see if compression should be used. /// - /// The stream where the compressed data have to be written. - /// The new stream. - Stream CreateStream(Stream outputStream); + /// + /// + bool ShouldCompressResponse(HttpContext context); } } diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionExtensions.cs index 720d9c30f0..cb827c4fd1 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionExtensions.cs @@ -3,7 +3,8 @@ using System; using Microsoft.AspNetCore.ResponseCompression; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.AspNetCore.Builder { @@ -13,22 +14,53 @@ namespace Microsoft.AspNetCore.Builder public static class ResponseCompressionExtensions { /// - /// Allows to compress HTTP Responses. + /// Add response compression services and enable compression for responses with the given MIME types. + /// + /// The for adding services. + /// Response Content-Type MIME types to enable compression for. + /// + public static IServiceCollection AddResponseCompression(this IServiceCollection services, params string[] mimeTypes) + { + return services.AddResponseCompression(options => + { + options.MimeTypes = mimeTypes; + }); + } + + /// + /// Add response compression services and configure the related options. + /// + /// The for adding services. + /// A delegate to configure the . + /// + public static IServiceCollection AddResponseCompression(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + services.TryAddTransient(); + return services; + } + + /// + /// Adds middleware for dynamically compressing HTTP Responses. /// /// The instance this method extends. - /// The . - public static IApplicationBuilder UseResponseCompression(this IApplicationBuilder builder, ResponseCompressionOptions options) + public static IApplicationBuilder UseResponseCompression(this IApplicationBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - return builder.UseMiddleware(Options.Create(options)); + return builder.UseMiddleware(); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs index aa0620b3fe..96e6c7af46 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionMiddleware.cs @@ -2,14 +2,9 @@ // 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 { @@ -20,9 +15,7 @@ namespace Microsoft.AspNetCore.ResponseCompression { private readonly RequestDelegate _next; - private readonly Dictionary _compressionProviders; - - private readonly Func _shouldCompressResponse; + private readonly IResponseCompressionProvider _provider; private readonly bool _enableHttps; @@ -30,35 +23,24 @@ namespace Microsoft.AspNetCore.ResponseCompression /// Initialize the Response Compression middleware. /// /// + /// /// - public ResponseCompressionMiddleware(RequestDelegate next, IOptions options) + public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider, IOptions options) { - if (options.Value.ShouldCompressResponse == null) + if (next == null) { - throw new ArgumentException($"{nameof(options.Value.ShouldCompressResponse)} is not provided in argument {nameof(options)}"); + throw new ArgumentNullException(nameof(next)); + } + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + if (options == null) + { + throw new ArgumentNullException(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); - + _provider = provider; _enableHttps = options.Value.EnableHttps; } @@ -69,11 +51,11 @@ namespace Microsoft.AspNetCore.ResponseCompression /// public async Task Invoke(HttpContext context) { - IResponseCompressionProvider compressionProvider = null; + ICompressionProvider compressionProvider = null; if (!context.Request.IsHttps || _enableHttps) { - compressionProvider = SelectProvider(context.Request.Headers[HeaderNames.AcceptEncoding]); + compressionProvider = _provider.GetCompressionProvider(context); } if (compressionProvider == null) @@ -84,7 +66,7 @@ namespace Microsoft.AspNetCore.ResponseCompression var bodyStream = context.Response.Body; - using (var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _shouldCompressResponse, compressionProvider)) + using (var bodyWrapperStream = new BodyWrapperStream(context.Response, bodyStream, _provider, compressionProvider)) { context.Response.Body = bodyWrapperStream; @@ -98,29 +80,5 @@ namespace Microsoft.AspNetCore.ResponseCompression } } } - - 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 index b3548bbb45..7d3b049984 100644 --- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs @@ -1,9 +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.Collections.Generic; -using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCompression { @@ -13,19 +11,13 @@ namespace Microsoft.AspNetCore.ResponseCompression public class ResponseCompressionOptions { /// - /// Called when an HTTP request accepts a compatible compression algorithm, and returns True - /// if the response should be compressed. + /// Response Content-Type MIME types to compress. /// - public Func ShouldCompressResponse { get; set; } + public IEnumerable MimeTypes { 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. + /// Indicates if responses over HTTPS connections should be compressed. The default is 'false'. + /// Enable compression on HTTPS connections may expose security problems. /// public bool EnableHttps { get; set; } = false; } diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs new file mode 100644 index 0000000000..64963b124a --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs @@ -0,0 +1,114 @@ +// 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.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.ResponseCompression +{ + /// + public class ResponseCompressionProvider : IResponseCompressionProvider + { + private readonly ICompressionProvider[] _providers; + private readonly HashSet _mimeTypes; + + /// + /// If no compression providers are specified then GZip is used by default. + /// + /// Compression providers to use, if any. + /// + public ResponseCompressionProvider(IEnumerable providers, IOptions options) + { + if (providers == null) + { + throw new ArgumentNullException(nameof(providers)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _providers = providers.ToArray(); + if (_providers.Length == 0) + { + _providers = new [] { new GzipCompressionProvider() }; + } + + if (options.Value.MimeTypes == null || !options.Value.MimeTypes.Any()) + { + throw new InvalidOperationException("No mime types specified"); + } + _mimeTypes = new HashSet(options.Value.MimeTypes, StringComparer.OrdinalIgnoreCase); + } + + /// + public virtual ICompressionProvider GetCompressionProvider(HttpContext context) + { + IList unsorted; + + // e.g. Accept-Encoding: gzip, deflate, sdch + var accept = context.Request.Headers[HeaderNames.AcceptEncoding]; + if (!StringValues.IsNullOrEmpty(accept) + && StringWithQualityHeaderValue.TryParseList(accept, out unsorted) + && unsorted != null && unsorted.Count > 0) + { + // TODO PERF: clients don't usually include quality values so this sort will not have any effect. Fast-path? + var sorted = unsorted + .Where(s => s.Quality.GetValueOrDefault(1) > 0) + .OrderByDescending(s => s.Quality.GetValueOrDefault(1)); + + foreach (var encoding in sorted) + { + // There will rarely be more than three providers, and there's only one by default + foreach (var provider in _providers) + { + if (string.Equals(provider.EncodingName, encoding.Value, StringComparison.OrdinalIgnoreCase)) + { + return provider; + } + } + + // Uncommon but valid options + if (string.Equals("*", encoding.Value, StringComparison.Ordinal)) + { + // Any + return _providers[0]; + } + if (string.Equals("identity", encoding.Value, StringComparison.OrdinalIgnoreCase)) + { + // No compression + return null; + } + } + } + + return null; + } + + /// + public virtual bool ShouldCompressResponse(HttpContext context) + { + var mimeType = context.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 _mimeTypes.Contains(mimeType); + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionUtils.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionUtils.cs deleted file mode 100644 index 1e917b595f..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionUtils.cs +++ /dev/null @@ -1,49 +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.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, StringComparer.OrdinalIgnoreCase); - - 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/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs index b526582b93..1e362fcc48 100644 --- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs @@ -3,15 +3,12 @@ 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; @@ -21,18 +18,6 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests { private const string TextPlain = "text/plain"; - [Fact] - public void Options_NullShouldCompressResponse_Throws() - { - Assert.Throws(() => - { - new ResponseCompressionMiddleware(null, Options.Create(new ResponseCompressionOptions() - { - ShouldCompressResponse = null - })); - }); - } - [Fact] public void Options_HttpsDisabledByDefault() { @@ -41,19 +26,6 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests Assert.False(options.EnableHttps); } - [Fact] - public void Options_EmptyProviderList_Throws() - { - Assert.Throws(() => - { - new ResponseCompressionMiddleware(null, Options.Create(new ResponseCompressionOptions() - { - ShouldCompressResponse = _ => true, - Providers = new IResponseCompressionProvider[0] - })); - }); - } - [Fact] public async Task Request_NoAcceptEncoding_Uncompressed() { @@ -78,6 +50,91 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests CheckResponseNotCompressed(response, expectedBodyLength: 100); } + [Fact] + public void NoMimeTypes_Throws() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddResponseCompression(); + }) + .Configure(app => + { + app.UseResponseCompression(); + app.Run(context => + { + context.Response.ContentType = TextPlain; + return context.Response.WriteAsync(new string('a', 100)); + }); + }); + + Assert.Throws(() => new TestServer(builder)); + } + + [Theory] + [InlineData("text/plain")] + [InlineData("text/PLAIN")] + [InlineData("text/plain; charset=ISO-8859-4")] + [InlineData("text/plain ; charset=ISO-8859-4")] + public async Task ContentType_WithCharset_Compress(string contentType) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddResponseCompression(TextPlain); + }) + .Configure(app => + { + app.UseResponseCompression(); + app.Run(context => + { + context.Response.ContentType = contentType; + return context.Response.WriteAsync(new string('a', 100)); + }); + }); + + 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); + + Assert.Equal(24, response.Content.ReadAsByteArrayAsync().Result.Length); + } + + [Theory] + [InlineData("")] + [InlineData("text/plain2")] + public async Task MimeTypes_OtherContentTypes_NoMatch(string contentType) + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddResponseCompression(TextPlain); + }) + .Configure(app => + { + app.UseResponseCompression(); + app.Run(context => + { + context.Response.ContentType = contentType; + return context.Response.WriteAsync(new string('a', 100)); + }); + }); + + 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); + + Assert.Equal(100, response.Content.ReadAsByteArrayAsync().Result.Length); + } + [Fact] public async Task Request_AcceptStar_Compressed() { @@ -134,7 +191,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests } [Fact] - public async Task Response_WithContentEncodingAlreadySet_NotCompressed() + public async Task Response_WithContentEncodingAlreadySet_Stacked() { var otherContentEncoding = "something"; @@ -143,9 +200,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests 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); + Assert.True(response.Content.Headers.ContentEncoding.Contains(otherContentEncoding)); + Assert.True(response.Content.Headers.ContentEncoding.Contains("gzip")); + Assert.Equal(24, response.Content.Headers.ContentLength); } [Theory] @@ -153,52 +210,47 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests [InlineData(true, 24)] public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength) { - var options = new ResponseCompressionOptions() - { - ShouldCompressResponse = _ => true, - Providers = new IResponseCompressionProvider[] + var builder = new WebHostBuilder() + .ConfigureServices(services => { - new GzipResponseCompressionProvider(CompressionLevel.Optimal) - }, - EnableHttps = enableHttps - }; + services.AddResponseCompression(options => + { + options.EnableHttps = enableHttps; + options.MimeTypes = new[] { TextPlain }; + }); + }) + .Configure(app => + { + app.UseResponseCompression(); + app.Run(context => + { + context.Response.ContentType = TextPlain; + return context.Response.WriteAsync(new string('a', 100)); + }); + }); - var middleware = new ResponseCompressionMiddleware(async context => - { - context.Response.ContentType = TextPlain; - await context.Response.WriteAsync(new string('a', 100)); - }, Options.Create(options)); + var server = new TestServer(builder); + server.BaseAddress = new Uri("https://localhost/"); + var client = server.CreateClient(); - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers[HeaderNames.AcceptEncoding] = "gzip"; - httpContext.Request.IsHttps = true; + var request = new HttpRequestMessage(HttpMethod.Get, ""); + request.Headers.AcceptEncoding.ParseAdd("gzip"); - httpContext.Response.Body = new MemoryStream(); + var response = await client.SendAsync(request); - await middleware.Invoke(httpContext); - - Assert.Equal(expectedLength, httpContext.Response.Body.Length); + Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.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() + .ConfigureServices(services => + { + services.AddResponseCompression(TextPlain); + }) .Configure(app => { - app.UseResponseCompression(options); + app.UseResponseCompression(); app.Run(context => { context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionUtilsTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionUtilsTest.cs deleted file mode 100644 index 73fd2caea3..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionUtilsTest.cs +++ /dev/null @@ -1,69 +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.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_Throws() - { - Assert.Throws(() => - { - ResponseCompressionUtils.CreateShouldCompressResponseDelegate(null); - }); - } - - [Fact] - public void CreateShouldCompressResponseDelegate_Empty_DontCompress() - { - 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")] - [InlineData("text/plain; charset=ISO-8859-4")] - [InlineData("text/plain ; charset=ISO-8859-4")] - public void CreateShouldCompressResponseDelegate_WithCharset_Compress(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")] - public void CreateShouldCompressResponseDelegate_OtherContentTypes_NoMatch(string contentType) - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.ContentType = contentType; - - var func = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(new string[] { TextPlain }); - - var result = func(httpContext); - - Assert.False(result); - } - } -}