Skip to content
This repository was archived by the owner on Dec 20, 2018. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions BasicMiddleware.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.ResponseCompression\Microsoft.AspNetCore.ResponseCompression.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" Version="$(MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(MicrosoftExtensionsDependencyInjectionPackageVersion)" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -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<string> 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);
}
}
}
3 changes: 3 additions & 0 deletions build/dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<BenchmarkDotNetPackageVersion>0.10.14</BenchmarkDotNetPackageVersion>
<InternalAspNetCoreSdkPackageVersion>2.2.0-preview1-17099</InternalAspNetCoreSdkPackageVersion>
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.2.0-preview1-34640</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
<MicrosoftAspNetCoreHostingAbstractionsPackageVersion>2.2.0-preview1-34640</MicrosoftAspNetCoreHostingAbstractionsPackageVersion>
<MicrosoftAspNetCoreHttpAbstractionsPackageVersion>2.2.0-preview1-34640</MicrosoftAspNetCoreHttpAbstractionsPackageVersion>
<MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.2.0-preview1-34640</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
Expand All @@ -15,6 +17,7 @@
<MicrosoftExtensionsConfigurationAbstractionsPackageVersion>2.2.0-preview1-34640</MicrosoftExtensionsConfigurationAbstractionsPackageVersion>
<MicrosoftExtensionsConfigurationBinderPackageVersion>2.2.0-preview1-34640</MicrosoftExtensionsConfigurationBinderPackageVersion>
<MicrosoftExtensionsConfigurationJsonPackageVersion>2.2.0-preview1-34640</MicrosoftExtensionsConfigurationJsonPackageVersion>
<MicrosoftExtensionsDependencyInjectionPackageVersion>2.2.0-preview1-34640</MicrosoftExtensionsDependencyInjectionPackageVersion>
<MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>2.2.0-preview1-34640</MicrosoftExtensionsFileProvidersAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.2.0-preview1-34640</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>2.2.0-preview1-34640</MicrosoftExtensionsLoggingConsolePackageVersion>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Brotli compression provider.
/// </summary>
public class BrotliCompressionProvider : ICompressionProvider
{
/// <summary>
/// Creates a new instance of <see cref="BrotliCompressionProvider"/> with options.
/// </summary>
/// <param name="options"></param>
public BrotliCompressionProvider(IOptions<BrotliCompressionProviderOptions> options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

Options = options.Value;
}

private BrotliCompressionProviderOptions Options { get; }

/// <inheritdoc />
public string EncodingName => "br";

/// <inheritdoc />
public bool SupportsFlush => true;

/// <inheritdoc />
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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Options for the <see cref="BrotliCompressionProvider"/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relating to options, can you add a comment to ResponseCompressionOptions saying the Providers's priority is based on their ordering in the list?

/// </summary>
public class BrotliCompressionProviderOptions : IOptions<BrotliCompressionProviderOptions>
{
/// <summary>
/// What level of compression to use for the stream. The default is <see cref="CompressionLevel.Fastest"/>.
/// </summary>
public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;

/// <inheritdoc />
BrotliCompressionProviderOptions IOptions<BrotliCompressionProviderOptions>.Value => this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public bool SupportsFlush
{
#if NET461
return false;
#elif NETSTANDARD2_0
#elif NETSTANDARD2_0 || NETCOREAPP2_1
return true;
#else
#error target frameworks need to be updated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Description>ASP.NET Core middleware for HTTP Response compression.</Description>
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
<TargetFrameworks>net461;netstandard2.0;netcoreapp2.1</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore</PackageTags>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class ResponseCompressionOptions
public bool EnableForHttps { get; set; } = false;

/// <summary>
/// The ICompressionProviders to use for responses.
/// The <see cref="ICompressionProvider"/> types to use for responses.
/// Providers are prioritized based on the order they are added.
/// </summary>
public CompressionProviderCollection Providers { get; } = new CompressionProviderCollection();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,17 @@ public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseC
if (_providers.Length == 0)
{
// Use the factory so it can resolve IOptions<GzipCompressionProviderOptions> 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++)
{
Expand All @@ -62,42 +72,76 @@ public ResponseCompressionProvider(IServiceProvider services, IOptions<ResponseC
/// <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)

if (StringValues.IsNullOrEmpty(accept))
{
// 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));
return null;
}

foreach (var encoding in sorted)
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<ProviderCandidate>();

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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is starting to become a lot of work on every response, and you'd get the same results most of the time, no? e.g. gzip, deflate, br or gzip, deflate are always going to evaluate to the same provider. Is it worth caching a few results based on the raw header string to avoid this redundant processing? Or at least having a fast path for headers that do not contain quality, then you can do a Contains for each available provider and be done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's much more work, it's just doing it the other way around by matching first and ordering after, but we can certainly try to be smarter to avoid some work 👍🏻

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth checking/considering making _providers a Dictionary? Besides that, I don't see much you can do.

}
}

// 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;
Expand Down Expand Up @@ -139,5 +183,39 @@ public bool CheckRequestAcceptsCompression(HttpContext context)
}
return !string.IsNullOrEmpty(context.Request.Headers[HeaderNames.AcceptEncoding]);
}

private readonly struct ProviderCandidate : IEquatable<ProviderCandidate>
{
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);
}
}
}
}
Loading