Response Compression in ASP.NET
Tags: programming, coding, csharp, dotnet
Published: 2023-07-19 07:45:32

Given that network bandwidth is a finite resource, optimizing its usage can markedly improve your application's performance. One effective strategy for maximizing network bandwidth utilization is response compression. It involves not only reducing the size of data transmitted from the server to the client, but can greatly improve the responsiveness of an application.

## Configuration

Enabling response compression in ASP.NET requires you to do two things:

1. Use `AddResponseCompression` to add the response compression service to the service container.
2. Use `UseResponseCompression` to enable response compression middleware in the request processing pipeline.

> ❗️ **Important**
> _Response compression middleware must be registered before other middleware that might write into the response._

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression();

var app = builder.Build();

app.UseResponseCompression();

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello World!");

app.Run();
```

## Compression with HTTPS

Due to inherent security risks, response compression for HTTPS connections is disabled by default. However, if you need to use this feature, it can be enabled by setting the `EnableForHttps` option to `true`.

```csharp
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
});
```

Using response compression over HTTPS can expose you to [CRIME](https://en.wikipedia.org/wiki/CRIME) and [BREACH](https://en.wikipedia.org/wiki/BREACH) attacks. These attacks can be mitigated in ASP.NET with antiforgery tokens.

The following is an example of a minimal API code that uses response compression with HTTPS enabled and implements antiforgery token checking:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery(options => options.HeaderName = "X-CSRF-TOKEN");

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseResponseCompression();

app.MapGet("/generate-token", (IAntiforgery antiforgery, HttpContext httpContext) =>
{
    var tokens = antiforgery.GetAndStoreTokens(httpContext);
    return Task.FromResult(tokens.RequestToken);
});

app.MapPost("/", async (IAntiforgery antiforgery, HttpContext httpContext) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(httpContext);
        return Results.Ok("Hello World");
    }
    catch
    {
        return Results.BadRequest("Invalid CSRF token");
    }
});

app.Run();
```

In this code, when you make a GET request to `/generate-token`, the server generates an antiforgery token and returns it. When you make a POST request to `/`, you should include this token in the request header with the name `X-CSRF-TOKEN`. The Antiforgery middleware will then validate the token. If the validation is successful, the response will be `Hello World`. Otherwise, the response will be `Invalid CSRF token`.

## Providers

A compression provider is a component that implements a specific compression algorithm. It is used to compress data before it is sent to the client.

When you invoke the `AddResponseCompression` method, two compression providers are included by default:

1. `BrotliCompressionProvider`, using the [Brotli](https://datatracker.ietf.org/doc/html/rfc7932) algorithm.
2. `GzipCompressionProvider`, using the [Gzip](https://datatracker.ietf.org/doc/html/rfc1952) algorithm.

Brotli is the default compression setting if the client supports this compressed data format. However, if the client does not support Brotli but does support Gzip compression, then Gzip becomes the default compression method.

Also, it is important to note that when you add a specific compression provider, other providers will not be included automatically. As an instance, if you have only explicitly included the Gzip compression provider, the system will not add any other compression providers.

Here is an example where you enable response compression for HTTPS requests and add the response compression providers Brotli and Gzip:

```csharp
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
});
```

You can set the compression level with `BrotliCompressionProviderOptions` and `GzipCompressionProviderOptions`. By default, both Brotli and Gzip compression providers use the fastest compression level, known as `CompressionLevel.Fastest`. This may not result in the most efficient compression. If you are aiming for the best compression, you should adjust the response compression middleware settings for optimal compression.

| Compression Level | Value | Description |
| ----------------- | ----- | ----------- |
| Optimal           | 0     | The compression operation should optimally balance compression speed and output size. |
| Fastest           | 1     | The compression operation should complete as quickly as possible, even if the resulting file is not optimally compressed. |
| NoCompression     | 2     | No compression should be performed on the file. |
| SmallestSize      | 3     | The compression operation should create output as small as possible, even if the operation takes a longer time to complete. |

```csharp
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Fastest;
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.SmallestSize;
});
```

## Custom providers

A custom compression provider is a class that inherits from the `ICompressionProvider` interface to provide a custom compression method for HTTP responses.

Unlike the other examples, this time I decided to create a real provider that you can reuse in your applications. The provider I am going to make is `DeflateCompressionProvider`, which takes advantage of the [Deflate](https://datatracker.ietf.org/doc/html/rfc1951) algorithm.

First, I define the options that I will use in the provider. I implement within it the `Level` property, specifying which compression level to use for the stream. The default setting is `Fastest`.

```csharp
public class DeflateCompressionProviderOptions : IOptions<DeflateCompressionProviderOptions>
{
    public CompressionLevel Level { get; set; } = CompressionLevel.Fastest;
    
    DeflateCompressionProviderOptions IOptions<DeflateCompressionProviderOptions>.Value => this;
}
```

Next, I create the compression provider using the built-in `DeflateStream` class as the compression algorithm, specifying in its constructor:
- the stream to be compressed;
- one of the values in the `Level` enumeration property of options;
- `true` to leave the stream object open after disposing the `DeflateStream` object.

In addition, I specify in the `EncodingName` property the encoding name that will be used in the `Accept-Encoding` request header and the `Content-Encoding` response header. I also set the `SupportsFlush` property to `true` with which I go to indicate whether the specified provider supports `Flush` and `FlushAsync`.

```csharp
public class DeflateCompressionProvider : ICompressionProvider
{
    public DeflateCompressionProvider(IOptions<DeflateCompressionProviderOptions> options)
    {
        ArgumentNullException.ThrowIfNull(nameof(options));

        Options = options.Value;
    }

    private DeflateCompressionProviderOptions Options { get; }
    
    public string EncodingName => "deflate";
    public bool SupportsFlush => true;
    
    public Stream CreateStream(Stream outputStream)
        => new DeflateStream(outputStream, Options.Level, leaveOpen: true);
}
```

Finally, I add the newly created compression provider into the application when I configure response compression:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<DeflateCompressionProvider>();
});

builder.Services.Configure<DeflateCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Optimal;
});

var app = builder.Build();

app.UseHttpsRedirection();

app.UseResponseCompression();

app.MapGet("/", () => "Hello World!");

app.Run();
```

## MIME types

The middleware for response compression provides a default collection of MIME types that can be compressed. For a comprehensive list of supported MIME types, refer to the [source code](https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/ResponseCompression/src/ResponseCompressionDefaults.cs).

You can modify or supplement the MIME types by using `ResponseCompressionOptions.MimeTypes`.

> ⚠️ **Warning**
> _Wildcard MIME types, such as `text/*`, are not supported._

In the following example, a MIME type is added for `image/svg+xml` in order to compress .svg files:

```csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/svg+xml" });
});

var app = builder.Build();

app.UseResponseCompression();
```

## Benchmark

For a more in-depth understanding of how much bandwidth can be saved, let's use the following minimal API:

```csharp
app.MapGet("/", () => Results.Ok(
    Enumerable.Range(1, 500).Select(num => new
    {
        Id = num, 
        Description = $"Hello World #{num}", 
        DayOfWeek = (DayOfWeek)Random.Shared.Next(0, 6)
    })));
```

Using the different compression providers listed above, including the one I made myself, and the different compression levels used, we examine the results obtained:

| Provider | Compression level | Response size | Percentage decrease |
| --------- | ----------------- | ------------- | ---------- |
| None     | None              | 28,93 KB      | baseline   |
| Gzip     | NoCompression     | 29,00 KB      | -0.2% (increase) |
| Gzip     | Fastest           | 3,60 KB       | 87,6%      |
| Gzip     | Optimal           | 3,32 KB       | 88,5%      |
| Gzip     | SmallestSize      | 3,06 KB       | 89,4%      |
| Brotli   | NoCompression     | 4,14 KB       | 85,7%      |
| Brotli   | Fastest           | 3,29 KB       | 88,6%      |
| Brotli   | Optimal           | 1,74 KB       | 94,0%      |
| Brotli   | SmallestSize      | 1,74 KB       | 94,0%      |
| Deflate  | NoCompression     | 28,99 KB      | -0.2% (increase) |
| Deflate  | Fastest           | 3,57 KB       | 87,7%      |
| Deflate  | Optimal           | 3,29 KB       | 88,6%      |
| Deflate  | SmallestSize      | 3,04 KB       | 89,5%      |

> 🔎 **Insight**
> _The formula for calculating the percentage decrease is as follows:_
>
>```
> Percentage decrease = ((Original size - Compressed size) / Original size) * 100
>```

The first entry, `None`, indicates the absence of a compression provider. It provides a baseline for understanding the effect of using different compression providers.

Next, the compression provider `Gzip` is examined. When it is used at the `NoCompression` level, it introduces a small increase in response size, indicating a small overhead associated with compression attempted, but not performed. When the compression level is increased to `Fastest`, the effectiveness of Gzip in reducing response size becomes apparent. Moving further to the `Optimal` level, Gzip demonstrates significant improvement, suggesting an ideal balance between compression speed and efficiency. At the `SmallestSize` level, Gzip attempts to reduce response size as much as possible, regardless of the time required for compression.

The compression provider `Brotli` shows a different behavior. At the `NoCompression` level, Brotli is much more efficient than Gzip. When set at the `Fastest` level, Brotli is shown to be very effective in reducing response size. Interestingly, at both the `Optimal` and `SmallestSize` levels, Brotli outperforms Gzip, achieving significantly better compression efficiency.

Finally, we analyze the compression provider `Deflate`. Like Gzip, at the `NoCompression` level it slightly increases the size of the response. Instead at the `Fastest`, `Optimal`, and `SmallestSize` levels, Deflate achieves similar compression efficiency to Gzip.

In summary, these results underscore the effectiveness of various compression providers in optimizing ASP.NET responses. It is particularly noteworthy that **Brotli emerges as the most efficient provider**, especially at the `Optimal` and `SmallestSize` compression levels.

## Conclusion

In this article, we explored optimizing ASP.NET application performance through response compression. We discussed how to configure and enable response compression, highlighting the need to balance performance with security considerations, especially in HTTPS contexts.

We also examined the use of different compression providers like Brotli and Gzip, emphasizing how the right provider choice can significantly impact performance. Additionally, we considered implementing custom compression providers for specific needs.

Concluding, through practical benchmarks, we demonstrated the effectiveness of various compression methods, underscoring the importance of choosing the most suitable solution to optimize bandwidth usage and enhance application responsiveness.

## References

- [Response compression in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/performance/response-compression?view=aspnetcore-7.0)