diff --git a/BasicMiddleware.sln b/BasicMiddleware.sln
index 8810201f5a..4bfc2ad2c5 100644
--- a/BasicMiddleware.sln
+++ b/BasicMiddleware.sln
@@ -76,6 +76,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HostFi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HostFiltering", "src\Microsoft.AspNetCore.HostFiltering\Microsoft.AspNetCore.HostFiltering.csproj", "{762F7276-C916-4111-A6C0-41668ABB3823}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{C6DA6317-30FC-42FE-891C-64E75D88FF12}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCompression.Benchmarks", "benchmarks\Microsoft.AspNetCore.ResponseCompression.Benchmarks\Microsoft.AspNetCore.ResponseCompression.Benchmarks.csproj", "{5AF10E85-5076-40B9-84CF-9830B585ABE5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -154,6 +158,10 @@ Global
{762F7276-C916-4111-A6C0-41668ABB3823}.Debug|Any CPU.Build.0 = Debug|Any CPU
{762F7276-C916-4111-A6C0-41668ABB3823}.Release|Any CPU.ActiveCfg = Release|Any CPU
{762F7276-C916-4111-A6C0-41668ABB3823}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5AF10E85-5076-40B9-84CF-9830B585ABE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5AF10E85-5076-40B9-84CF-9830B585ABE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5AF10E85-5076-40B9-84CF-9830B585ABE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5AF10E85-5076-40B9-84CF-9830B585ABE5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -178,6 +186,7 @@ Global
{5CEA6F31-A829-4A02-8CD5-EC3DDD4CC1EA} = {59A9B64C-E9BE-409E-89A2-58D72E2918F5}
{4BC947ED-13B8-4BE6-82A4-96A48D86980B} = {8437B0F3-3894-4828-A945-A9187F37631D}
{762F7276-C916-4111-A6C0-41668ABB3823} = {A5076D28-FA7E-4606-9410-FEDD0D603527}
+ {5AF10E85-5076-40B9-84CF-9830B585ABE5} = {C6DA6317-30FC-42FE-891C-64E75D88FF12}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4518E9CE-3680-4E05-9259-B64EA7807158}
diff --git a/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/AssemblyInfo.cs b/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/AssemblyInfo.cs
new file mode 100644
index 0000000000..409fcf814a
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+// 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.
+
+[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
diff --git a/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks.csproj b/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks.csproj
new file mode 100644
index 0000000000..5dbd63c35a
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ netcoreapp2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/ResponseCompressionProviderBenchmark.cs b/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/ResponseCompressionProviderBenchmark.cs
new file mode 100644
index 0000000000..4e4b78fcb7
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.ResponseCompression.Benchmarks/ResponseCompressionProviderBenchmark.cs
@@ -0,0 +1,57 @@
+// 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.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
+
+namespace Microsoft.AspNetCore.ResponseCompression.Benchmarks
+{
+ public class ResponseCompressionProviderBenchmark
+ {
+ [GlobalSetup]
+ public void GlobalSetup()
+ {
+ var services = new ServiceCollection()
+ .AddOptions()
+ .AddResponseCompression()
+ .BuildServiceProvider();
+
+ var options = new ResponseCompressionOptions();
+
+ Provider = new ResponseCompressionProvider(services, Options.Create(options));
+ }
+
+ [ParamsSource(nameof(EncodingStrings))]
+ public string AcceptEncoding { get; set; }
+
+ public static IEnumerable EncodingStrings()
+ {
+ return new[]
+ {
+ "gzip;q=0.8, compress;q=0.6, br;q=0.4",
+ "gzip, compress, br",
+ "br, compress, gzip",
+ "gzip, compress",
+ "identity",
+ "*"
+ };
+ }
+
+ public ResponseCompressionProvider Provider { get; set; }
+
+ [Benchmark]
+ public ICompressionProvider GetCompressionProvider()
+ {
+ var context = new DefaultHttpContext();
+
+ context.Request.Headers[HeaderNames.AcceptEncoding] = AcceptEncoding;
+
+ return Provider.GetCompressionProvider(context);
+ }
+ }
+}
\ No newline at end of file
diff --git a/build/dependencies.props b/build/dependencies.props
index 3ab03afc13..7147597eac 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -3,7 +3,9 @@
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ 0.10.14
2.2.0-preview1-17099
+ 2.2.0-preview1-34640
2.2.0-preview1-34640
2.2.0-preview1-34640
2.2.0-preview1-34640
@@ -15,6 +17,7 @@
2.2.0-preview1-34640
2.2.0-preview1-34640
2.2.0-preview1-34640
+ 2.2.0-preview1-34640
2.2.0-preview1-34640
2.2.0-preview1-34640
2.2.0-preview1-34640
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/BrotliCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/BrotliCompressionProvider.cs
new file mode 100644
index 0000000000..20a88dd30a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/BrotliCompressionProvider.cs
@@ -0,0 +1,51 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.IO.Compression;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// Brotli compression provider.
+ ///
+ public class BrotliCompressionProvider : ICompressionProvider
+ {
+ ///
+ /// Creates a new instance of with options.
+ ///
+ ///
+ public BrotliCompressionProvider(IOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ Options = options.Value;
+ }
+
+ private BrotliCompressionProviderOptions Options { get; }
+
+ ///
+ public string EncodingName => "br";
+
+ ///
+ public bool SupportsFlush => true;
+
+ ///
+ public Stream CreateStream(Stream outputStream)
+ {
+#if NETCOREAPP2_1
+ return new BrotliStream(outputStream, Options.Level, leaveOpen: true);
+#elif NET461 || NETSTANDARD2_0
+ // Brotli is only supported in .NET Core 2.1+
+ throw new PlatformNotSupportedException();
+#else
+#error Target frameworks need to be updated.
+#endif
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/BrotliCompressionProviderOptions.cs b/src/Microsoft.AspNetCore.ResponseCompression/BrotliCompressionProviderOptions.cs
new file mode 100644
index 0000000000..029f22b854
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCompression/BrotliCompressionProviderOptions.cs
@@ -0,0 +1,22 @@
+// 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.Compression;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.ResponseCompression
+{
+ ///
+ /// Options for the
+ ///
+ public class BrotliCompressionProviderOptions : IOptions
+ {
+ ///
+ /// What level of compression to use for the stream. The default is .
+ ///
+ public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;
+
+ ///
+ BrotliCompressionProviderOptions IOptions.Value => this;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/GzipCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/GzipCompressionProvider.cs
index e825b37976..474f255111 100644
--- a/src/Microsoft.AspNetCore.ResponseCompression/GzipCompressionProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCompression/GzipCompressionProvider.cs
@@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.ResponseCompression
{
#if NET461
return false;
-#elif NETSTANDARD2_0
+#elif NETSTANDARD2_0 || NETCOREAPP2_1
return true;
#else
#error target frameworks need to be updated
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj b/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj
index e652961322..d39f7db89a 100644
--- a/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj
+++ b/src/Microsoft.AspNetCore.ResponseCompression/Microsoft.AspNetCore.ResponseCompression.csproj
@@ -2,7 +2,7 @@
ASP.NET Core middleware for HTTP Response compression.
- net461;netstandard2.0
+ net461;netstandard2.0;netcoreapp2.1
true
aspnetcore
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs
index c6b33c9737..6ec4fb62b4 100644
--- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs
+++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionOptions.cs
@@ -22,7 +22,8 @@ namespace Microsoft.AspNetCore.ResponseCompression
public bool EnableForHttps { get; set; } = false;
///
- /// The ICompressionProviders to use for responses.
+ /// The types to use for responses.
+ /// Providers are prioritized based on the order they are added.
///
public CompressionProviderCollection Providers { get; } = new CompressionProviderCollection();
}
diff --git a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs
index e5f577f00d..3ed2dd058a 100644
--- a/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCompression/ResponseCompressionProvider.cs
@@ -38,7 +38,17 @@ namespace Microsoft.AspNetCore.ResponseCompression
if (_providers.Length == 0)
{
// Use the factory so it can resolve IOptions from DI.
- _providers = new ICompressionProvider[] { new CompressionProviderFactory(typeof(GzipCompressionProvider)) };
+ _providers = new ICompressionProvider[]
+ {
+#if NETCOREAPP2_1
+ new CompressionProviderFactory(typeof(BrotliCompressionProvider)),
+#elif NET461 || NETSTANDARD2_0
+ // Brotli is only supported in .NET Core 2.1+
+#else
+#error Target frameworks need to be updated.
+#endif
+ new CompressionProviderFactory(typeof(GzipCompressionProvider)),
+ };
}
for (var i = 0; i < _providers.Length; i++)
{
@@ -62,42 +72,76 @@ namespace Microsoft.AspNetCore.ResponseCompression
///
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)
+ if (StringValues.IsNullOrEmpty(accept))
+ {
+ return null;
+ }
+
+ if (StringWithQualityHeaderValue.TryParseList(accept, out var encodings))
+ {
+ if (encodings.Count == 0)
{
- // There will rarely be more than three providers, and there's only one by default
- foreach (var provider in _providers)
+ return null;
+ }
+
+ var candidates = new HashSet();
+
+ foreach (var encoding in encodings)
+ {
+ var encodingName = encoding.Value;
+ var quality = encoding.Quality.GetValueOrDefault(1);
+
+ if (quality < double.Epsilon)
{
- if (StringSegment.Equals(provider.EncodingName, encoding.Value, StringComparison.OrdinalIgnoreCase))
+ continue;
+ }
+
+ for (int i = 0; i < _providers.Length; i++)
+ {
+ var provider = _providers[i];
+
+ if (StringSegment.Equals(provider.EncodingName, encodingName, StringComparison.OrdinalIgnoreCase))
{
- return provider;
+ candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider));
}
}
// Uncommon but valid options
- if (StringSegment.Equals("*", encoding.Value, StringComparison.Ordinal))
+ if (StringSegment.Equals("*", encodingName, StringComparison.Ordinal))
{
- // Any
- return _providers[0];
+ for (int i = 0; i < _providers.Length; i++)
+ {
+ var provider = _providers[i];
+
+ // Any provider is a candidate.
+ candidates.Add(new ProviderCandidate(provider.EncodingName, quality, i, provider));
+ }
+
+ break;
}
- if (StringSegment.Equals("identity", encoding.Value, StringComparison.OrdinalIgnoreCase))
+
+ if (StringSegment.Equals("identity", encodingName, StringComparison.OrdinalIgnoreCase))
{
- // No compression
- return null;
+ // We add 'identity' to the list of "candidates" with a very low priority and no provider.
+ // This will allow it to be ordered based on its quality (and priority) later in the method.
+ candidates.Add(new ProviderCandidate(encodingName.Value, quality, priority: int.MaxValue, provider: null));
}
}
+
+ if (candidates.Count <= 1)
+ {
+ return candidates.ElementAtOrDefault(0).Provider;
+ }
+
+ var accepted = candidates
+ .OrderByDescending(x => x.Quality)
+ .ThenBy(x => x.Priority)
+ .First();
+
+ return accepted.Provider;
}
return null;
@@ -139,5 +183,39 @@ namespace Microsoft.AspNetCore.ResponseCompression
}
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]);
}
+
+ private readonly struct ProviderCandidate : IEquatable
+ {
+ public ProviderCandidate(string encodingName, double quality, int priority, ICompressionProvider provider)
+ {
+ EncodingName = encodingName;
+ Quality = quality;
+ Priority = priority;
+ Provider = provider;
+ }
+
+ public string EncodingName { get; }
+
+ public double Quality { get; }
+
+ public int Priority { get; }
+
+ public ICompressionProvider Provider { get; }
+
+ public bool Equals(ProviderCandidate other)
+ {
+ return string.Equals(EncodingName, other.EncodingName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is ProviderCandidate candidate && Equals(candidate);
+ }
+
+ public override int GetHashCode()
+ {
+ return StringComparer.OrdinalIgnoreCase.GetHashCode(EncodingName);
+ }
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
index 52eb965cf0..5a2a17a53c 100644
--- a/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
+++ b/test/Microsoft.AspNetCore.ResponseCompression.Tests/ResponseCompressionMiddlewareTest.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
+using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
@@ -23,6 +24,26 @@ namespace Microsoft.AspNetCore.ResponseCompression.Tests
{
private const string TextPlain = "text/plain";
+ public static IEnumerable