#108 Change ResponseCompression to be DI centric

This commit is contained in:
Chris R 2016-09-30 10:15:46 -07:00
parent 3e119a87be
commit 9bc8a83a39
14 changed files with 374 additions and 311 deletions

View File

@ -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;
}
}
}

View File

@ -1,22 +1,9 @@
{ {
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:49658/",
"sslPort": 0
}
},
"profiles": { "profiles": {
"IIS Express": { "ResponseCompressionSample": {
"commandName": "IISExpress", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"environmentVariables": { "launchUrl": "http://localhost:5000/",
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"web": {
"commandName": "web",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@ -5,21 +5,26 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.DependencyInjection;
namespace ResponseCompressionSample namespace ResponseCompressionSample
{ {
public class Startup public class Startup
{ {
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<ICompressionProvider, GzipCompressionProvider>();
services.AddTransient<ICompressionProvider, CustomCompressionProvider>();
services.AddResponseCompression("text/plain", "text/html");
}
public void Configure(IApplicationBuilder app) public void Configure(IApplicationBuilder app)
{ {
app.UseResponseCompression(new ResponseCompressionOptions() app.UseResponseCompression();
{
ShouldCompressResponse = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(new string[] { "text/plain" })
});
app.Run(async context => app.Run(async context =>
{ {
context.Response.Headers["Content-Type"] = "text/plain"; context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(LoremIpsum.Text); await context.Response.WriteAsync(LoremIpsum.Text);
}); });
} }

View File

@ -2,12 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCompression namespace Microsoft.AspNetCore.ResponseCompression
@ -21,19 +19,19 @@ namespace Microsoft.AspNetCore.ResponseCompression
private readonly Stream _bodyOriginalStream; private readonly Stream _bodyOriginalStream;
private readonly Func<HttpContext, bool> _shouldCompressResponse; private readonly IResponseCompressionProvider _provider;
private readonly IResponseCompressionProvider _compressionProvider; private readonly ICompressionProvider _compressionProvider;
private bool _compressionChecked = false; private bool _compressionChecked = false;
private Stream _compressionStream = null; private Stream _compressionStream = null;
internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, Func<HttpContext, bool> shouldCompressResponse, IResponseCompressionProvider compressionProvider) internal BodyWrapperStream(HttpResponse response, Stream bodyOriginalStream, IResponseCompressionProvider provider, ICompressionProvider compressionProvider)
{ {
_response = response; _response = response;
_bodyOriginalStream = bodyOriginalStream; _bodyOriginalStream = bodyOriginalStream;
_shouldCompressResponse = shouldCompressResponse; _provider = provider;
_compressionProvider = compressionProvider; _compressionProvider = compressionProvider;
} }
@ -174,9 +172,8 @@ namespace Microsoft.AspNetCore.ResponseCompression
private bool IsCompressable() private bool IsCompressable()
{ {
return _response.Headers[HeaderNames.ContentRange] == StringValues.Empty && // The response is not partial return !_response.Headers.ContainsKey(HeaderNames.ContentRange) && // The response is not partial
_response.Headers[HeaderNames.ContentEncoding] == StringValues.Empty && // Not specific encoding already set _provider.ShouldCompressResponse(_response.HttpContext);
_shouldCompressResponse(_response.HttpContext);
} }
} }
} }

View File

@ -9,26 +9,27 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <summary> /// <summary>
/// GZIP compression provider. /// GZIP compression provider.
/// </summary> /// </summary>
public class GzipResponseCompressionProvider : IResponseCompressionProvider public class GzipCompressionProvider : ICompressionProvider
{ {
private readonly CompressionLevel _level;
/// <summary> /// <summary>
/// Initialize a new <see cref="GzipResponseCompressionProvider"/>. /// Initialize a new <see cref="GzipCompressionProvider"/>.
/// </summary> /// </summary>
/// <param name="level">The compression level.</param> public GzipCompressionProvider()
public GzipResponseCompressionProvider(CompressionLevel level)
{ {
_level = level;
} }
/// <inheritdoc /> /// <inheritdoc />
public string EncodingName { get; } = "gzip"; public string EncodingName => "gzip";
/// <summary>
/// What level of compression to use for the stream.
/// </summary>
public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;
/// <inheritdoc /> /// <inheritdoc />
public Stream CreateStream(Stream outputStream) public Stream CreateStream(Stream outputStream)
{ {
return new GZipStream(outputStream, _level, true); return new GZipStream(outputStream, Level, leaveOpen: true);
} }
} }
} }

View File

@ -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
{
/// <summary>
/// Provides a specific compression implementation to compress HTTP responses.
/// </summary>
public interface ICompressionProvider
{
/// <summary>
/// The encoding name used in the 'Accept-Encoding' request header and 'Content-Encoding' response header.
/// </summary>
string EncodingName { get; }
/// <summary>
/// Create a new compression stream.
/// </summary>
/// <param name="outputStream">The stream where the compressed data have to be written.</param>
/// <returns>The compression stream.</returns>
Stream CreateStream(Stream outputStream);
}
}

View File

@ -1,25 +1,27 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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 namespace Microsoft.AspNetCore.ResponseCompression
{ {
/// <summary> /// <summary>
/// Provides methods to be able to compress HTTP responses. /// Used to examine requests and responses to see if compression should be enabled.
/// </summary> /// </summary>
public interface IResponseCompressionProvider public interface IResponseCompressionProvider
{ {
/// <summary> /// <summary>
/// The name that will be searched in the 'Accept-Encoding' request header. /// Examines the request and selects an acceptable compression provider, if any.
/// </summary> /// </summary>
string EncodingName { get; } /// <param name="context"></param>
/// <returns>A compression provider or null if compression should not be used.</returns>
ICompressionProvider GetCompressionProvider(HttpContext context);
/// <summary> /// <summary>
/// Create a new compression stream. /// Examines the response on first write to see if compression should be used.
/// </summary> /// </summary>
/// <param name="outputStream">The stream where the compressed data have to be written.</param> /// <param name="context"></param>
/// <returns>The new stream.</returns> /// <returns></returns>
Stream CreateStream(Stream outputStream); bool ShouldCompressResponse(HttpContext context);
} }
} }

View File

@ -3,7 +3,8 @@
using System; using System;
using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Options; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.AspNetCore.Builder namespace Microsoft.AspNetCore.Builder
{ {
@ -13,22 +14,53 @@ namespace Microsoft.AspNetCore.Builder
public static class ResponseCompressionExtensions public static class ResponseCompressionExtensions
{ {
/// <summary> /// <summary>
/// Allows to compress HTTP Responses. /// Add response compression services and enable compression for responses with the given MIME types.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <param name="mimeTypes">Response Content-Type MIME types to enable compression for.</param>
/// <returns></returns>
public static IServiceCollection AddResponseCompression(this IServiceCollection services, params string[] mimeTypes)
{
return services.AddResponseCompression(options =>
{
options.MimeTypes = mimeTypes;
});
}
/// <summary>
/// Add response compression services and configure the related options.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <param name="configureOptions">A delegate to configure the <see cref="ResponseCompressionOptions"/>.</param>
/// <returns></returns>
public static IServiceCollection AddResponseCompression(this IServiceCollection services, Action<ResponseCompressionOptions> configureOptions)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
services.Configure(configureOptions);
services.TryAddTransient<IResponseCompressionProvider, ResponseCompressionProvider>();
return services;
}
/// <summary>
/// Adds middleware for dynamically compressing HTTP Responses.
/// </summary> /// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param> /// <param name="builder">The <see cref="IApplicationBuilder"/> instance this method extends.</param>
/// <param name="options">The <see cref="ResponseCompressionOptions"/>.</param> public static IApplicationBuilder UseResponseCompression(this IApplicationBuilder builder)
public static IApplicationBuilder UseResponseCompression(this IApplicationBuilder builder, ResponseCompressionOptions options)
{ {
if (builder == null) if (builder == null)
{ {
throw new ArgumentNullException(nameof(builder)); throw new ArgumentNullException(nameof(builder));
} }
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
return builder.UseMiddleware<ResponseCompressionMiddleware>(Options.Create(options)); return builder.UseMiddleware<ResponseCompressionMiddleware>();
} }
} }
} }

View File

@ -2,14 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCompression namespace Microsoft.AspNetCore.ResponseCompression
{ {
@ -20,9 +15,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly Dictionary<string, IResponseCompressionProvider> _compressionProviders; private readonly IResponseCompressionProvider _provider;
private readonly Func<HttpContext, bool> _shouldCompressResponse;
private readonly bool _enableHttps; private readonly bool _enableHttps;
@ -30,35 +23,24 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// 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="options"></param> /// <param name="options"></param>
public ResponseCompressionMiddleware(RequestDelegate next, IOptions<ResponseCompressionOptions> options) public ResponseCompressionMiddleware(RequestDelegate next, IResponseCompressionProvider provider, IOptions<ResponseCompressionOptions> 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; _next = next;
_provider = provider;
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; _enableHttps = options.Value.EnableHttps;
} }
@ -69,11 +51,11 @@ namespace Microsoft.AspNetCore.ResponseCompression
/// <returns></returns> /// <returns></returns>
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext context)
{ {
IResponseCompressionProvider compressionProvider = null; ICompressionProvider compressionProvider = null;
if (!context.Request.IsHttps || _enableHttps) if (!context.Request.IsHttps || _enableHttps)
{ {
compressionProvider = SelectProvider(context.Request.Headers[HeaderNames.AcceptEncoding]); compressionProvider = _provider.GetCompressionProvider(context);
} }
if (compressionProvider == null) if (compressionProvider == null)
@ -84,7 +66,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
var bodyStream = context.Response.Body; 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; context.Response.Body = bodyWrapperStream;
@ -98,29 +80,5 @@ namespace Microsoft.AspNetCore.ResponseCompression
} }
} }
} }
private IResponseCompressionProvider SelectProvider(StringValues acceptEncoding)
{
IList<StringWithQualityHeaderValue> 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;
}
} }
} }

View File

@ -1,9 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // 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.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCompression namespace Microsoft.AspNetCore.ResponseCompression
{ {
@ -13,19 +11,13 @@ namespace Microsoft.AspNetCore.ResponseCompression
public class ResponseCompressionOptions public class ResponseCompressionOptions
{ {
/// <summary> /// <summary>
/// Called when an HTTP request accepts a compatible compression algorithm, and returns True /// Response Content-Type MIME types to compress.
/// if the response should be compressed.
/// </summary> /// </summary>
public Func<HttpContext, bool> ShouldCompressResponse { get; set; } public IEnumerable<string> MimeTypes { get; set; }
/// <summary> /// <summary>
/// The compression providers. If 'null', the GZIP provider is set as default. /// Indicates if responses over HTTPS connections should be compressed. The default is 'false'.
/// </summary> /// Enable compression on HTTPS connections may expose security problems.
public IEnumerable<IResponseCompressionProvider> Providers { get; set; }
/// <summary>
/// 'False' to enable compression only on HTTP requests. Enable compression on HTTPS requests
/// may lead to security problems.
/// </summary> /// </summary>
public bool EnableHttps { get; set; } = false; public bool EnableHttps { get; set; } = false;
} }

View File

@ -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
{
/// <inheritdoc />
public class ResponseCompressionProvider : IResponseCompressionProvider
{
private readonly ICompressionProvider[] _providers;
private readonly HashSet<string> _mimeTypes;
/// <summary>
/// If no compression providers are specified then GZip is used by default.
/// </summary>
/// <param name="providers">Compression providers to use, if any.</param>
/// <param name="options"></param>
public ResponseCompressionProvider(IEnumerable<ICompressionProvider> providers, IOptions<ResponseCompressionOptions> 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<string>(options.Value.MimeTypes, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
public virtual ICompressionProvider GetCompressionProvider(HttpContext context)
{
IList<StringWithQualityHeaderValue> 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;
}
/// <inheritdoc />
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);
}
}
}

View File

@ -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
{
/// <summary>
/// Response Compression middleware utility methods.
/// </summary>
public static class ResponseCompressionUtils
{
/// <summary>
/// Create a delegate that propose to compress response, depending on a list of authorized
/// MIME types for the HTTP response.
/// </summary>
public static Func<HttpContext, bool> CreateShouldCompressResponseDelegate(IEnumerable<string> mimeTypes)
{
if (mimeTypes == null)
{
throw new ArgumentNullException(nameof(mimeTypes));
}
var mimeTypeSet = new HashSet<string>(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);
};
}
}
}

View File

@ -3,15 +3,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
@ -21,18 +18,6 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{ {
private const string TextPlain = "text/plain"; private const string TextPlain = "text/plain";
[Fact]
public void Options_NullShouldCompressResponse_Throws()
{
Assert.Throws<ArgumentException>(() =>
{
new ResponseCompressionMiddleware(null, Options.Create(new ResponseCompressionOptions()
{
ShouldCompressResponse = null
}));
});
}
[Fact] [Fact]
public void Options_HttpsDisabledByDefault() public void Options_HttpsDisabledByDefault()
{ {
@ -41,19 +26,6 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
Assert.False(options.EnableHttps); Assert.False(options.EnableHttps);
} }
[Fact]
public void Options_EmptyProviderList_Throws()
{
Assert.Throws<ArgumentException>(() =>
{
new ResponseCompressionMiddleware(null, Options.Create(new ResponseCompressionOptions()
{
ShouldCompressResponse = _ => true,
Providers = new IResponseCompressionProvider[0]
}));
});
}
[Fact] [Fact]
public async Task Request_NoAcceptEncoding_Uncompressed() public async Task Request_NoAcceptEncoding_Uncompressed()
{ {
@ -78,6 +50,91 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
CheckResponseNotCompressed(response, expectedBodyLength: 100); 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<InvalidOperationException>(() => 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] [Fact]
public async Task Request_AcceptStar_Compressed() public async Task Request_AcceptStar_Compressed()
{ {
@ -134,7 +191,7 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
} }
[Fact] [Fact]
public async Task Response_WithContentEncodingAlreadySet_NotCompressed() public async Task Response_WithContentEncodingAlreadySet_Stacked()
{ {
var otherContentEncoding = "something"; var otherContentEncoding = "something";
@ -143,9 +200,9 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
r.Headers[HeaderNames.ContentEncoding] = otherContentEncoding; r.Headers[HeaderNames.ContentEncoding] = otherContentEncoding;
}); });
Assert.NotNull(response.Headers.GetValues(HeaderNames.ContentMD5)); Assert.True(response.Content.Headers.ContentEncoding.Contains(otherContentEncoding));
Assert.Single(response.Content.Headers.ContentEncoding, otherContentEncoding); Assert.True(response.Content.Headers.ContentEncoding.Contains("gzip"));
Assert.Equal(50, response.Content.Headers.ContentLength); Assert.Equal(24, response.Content.Headers.ContentLength);
} }
[Theory] [Theory]
@ -153,52 +210,47 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
[InlineData(true, 24)] [InlineData(true, 24)]
public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength) public async Task Request_Https_CompressedIfEnabled(bool enableHttps, int expectedLength)
{ {
var options = new ResponseCompressionOptions() var builder = new WebHostBuilder()
{ .ConfigureServices(services =>
ShouldCompressResponse = _ => true,
Providers = new IResponseCompressionProvider[]
{ {
new GzipResponseCompressionProvider(CompressionLevel.Optimal) services.AddResponseCompression(options =>
}, {
EnableHttps = enableHttps 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 => var server = new TestServer(builder);
{ server.BaseAddress = new Uri("https://localhost/");
context.Response.ContentType = TextPlain; var client = server.CreateClient();
await context.Response.WriteAsync(new string('a', 100));
}, Options.Create(options));
var httpContext = new DefaultHttpContext(); var request = new HttpRequestMessage(HttpMethod.Get, "");
httpContext.Request.Headers[HeaderNames.AcceptEncoding] = "gzip"; request.Headers.AcceptEncoding.ParseAdd("gzip");
httpContext.Request.IsHttps = true;
httpContext.Response.Body = new MemoryStream(); var response = await client.SendAsync(request);
await middleware.Invoke(httpContext); Assert.Equal(expectedLength, response.Content.ReadAsByteArrayAsync().Result.Length);
Assert.Equal(expectedLength, httpContext.Response.Body.Length);
} }
private Task<HttpResponseMessage> InvokeMiddleware(int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action<HttpResponse> addResponseAction = null) private Task<HttpResponseMessage> InvokeMiddleware(int uncompressedBodyLength, string[] requestAcceptEncodings, string responseType, Action<HttpResponse> 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() var builder = new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddResponseCompression(TextPlain);
})
.Configure(app => .Configure(app =>
{ {
app.UseResponseCompression(options); app.UseResponseCompression();
app.Run(context => app.Run(context =>
{ {
context.Response.Headers[HeaderNames.ContentMD5] = "MD5"; context.Response.Headers[HeaderNames.ContentMD5] = "MD5";

View File

@ -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<ArgumentNullException>(() =>
{
ResponseCompressionUtils.CreateShouldCompressResponseDelegate(null);
});
}
[Fact]
public void CreateShouldCompressResponseDelegate_Empty_DontCompress()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.ContentType = TextPlain;
var func = ResponseCompressionUtils.CreateShouldCompressResponseDelegate(Enumerable.Empty<string>());
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);
}
}
}