Compress Blazor WebAssembly (WASM) Web API/REST API HTTPS Output

This example uses ASP.NET Core 3.1. It should be compatible with later versions of .NET including .NET 5 and .NET 6. See how to enable Web API HTTPS compression for use with JavaScript.

Compressing Web API (REST API) output makes a Blazor WASM application so much more responsive (there is a security issue called BREACH). Enable site-wide ASP.NET HTTPS compression for HTTPS connections.

This code will only compress the Web API. The rest of the site remains as is.

The key to making this work is converting all objects to a JSON string and then compressing it with the BrotliStream first and then the GZipStream second if the client does not accept Brotli. Then the compressed data is sent to the client.

Here is the snipped of code that does all of this work.

var json = System.Text.Json.JsonSerializer.Serialize(data); // CONVERT DATA TO JSON STRING
if (!string.IsNullOrEmpty(Request.Headers["Accept-Encoding"])) // CHECK THAT THE REQUEST SUPPORTS COMPRESSION
{
	var encodings = Request.Headers["Accept-Encoding"].ToString().Split(',');
	for(int i = 0; i < encodings.Length; i++)
		encodings[i] = encodings[i].Trim();
	if (Array.IndexOf(encodings, "br") > -1)
	{
		Response.Headers.Append("Content-Encoding", "br");
		var compressedBytes = await Compressor.BrotliCompressBytesAsync(System.Text.Encoding.UTF8.GetBytes(json), cancel);
		return File(compressedBytes, "application/json"); // RETURN THE DATA AND ADD THE CONTENT TYPE
	}
	if (Array.IndexOf(encodings, "gzip") > -1)
	{
		Response.Headers.Append("Content-Encoding", "gzip");
		var compressedBytes = await Compressor.GZipCompressBytesAsync(System.Text.Encoding.UTF8.GetBytes(json), cancel);
		return File(compressedBytes, "application/json"); // RETURN THE DATA AND ADD THE CONTENT TYPE
	}
}
Response.ContentType = "application/json"; // ADD THE CONTENT TYPE
return Content(json); // RETURN THE NON-COMPRESSED DATA

USE THIS UTILITY TO TEST THE HTTP COMPRESSION.

Here is the WeatherForecast controller's code which should return compressed data over an HTTPS connection for a Blazor WASM client.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;

namespace ProjectName.Server.Controllers
{
	[ApiController]
	[Route("[controller]")]
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> logger;

		public WeatherForecastController(ILogger<WeatherForecastController> logger)
		{
			this.logger = logger;
		}

		[HttpGet]
		public async Task<IActionResult> Get(System.Threading.CancellationToken cancel) // NOTE: RETURN TYPE IS IActionResult
		{
			var rng = new Random();
			var data = Enumerable.Range(1, 2000).Select(index => new WeatherForecast // NOTE: NOTICE THE SIZE OF THE ARRAY
			{
				Date = DateTime.Now.AddDays(index),
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)]
			});
			var json = System.Text.Json.JsonSerializer.Serialize(data);
			if (!string.IsNullOrEmpty(Request.Headers["Accept-Encoding"]))
			{
				var encodings = Request.Headers["Accept-Encoding"].ToString().Split(',');
				for(int i = 0; i < encodings.Length; i++)
					encodings[i] = encodings[i].Trim();
				if (Array.IndexOf(encodings, "br") > -1) // PREFER BROTLI!!
				{
					Response.Headers.Append("Content-Encoding", "br");
					var compressedBytes = await Compressor.BrotliCompressBytesAsync(System.Text.Encoding.UTF8.GetBytes(json), cancel);
					return File(compressedBytes, "application/json");
				}
				if (Array.IndexOf(encodings, "gzip") > -1) // FALLBACK TO GZIP
				{
					Response.Headers.Append("Content-Encoding", "gzip");
					var compressedBytes = await Compressor.GZipCompressBytesAsync(System.Text.Encoding.UTF8.GetBytes(json), cancel);
					return File(compressedBytes, "application/json");
				}
			}
			Response.ContentType = "application/json"; // ADD THE CONTENT TYPE
			return Content(json); // return non-compressed data
		}
	}

	internal class Compressor
	{
		public static async Task<byte[]> BrotliCompressBytesAsync(byte[] bytes, System.Threading.CancellationToken cancel)
		{
			using (var outputStream = new MemoryStream())
			{
				using (var compressionStream = new BrotliStream(outputStream, CompressionLevel.Optimal))
				{
					await compressionStream.WriteAsync(bytes, 0, bytes.Length, cancel);
				}
				return outputStream.ToArray();
			}
		}
		public static async Task<byte[]> GZipCompressBytesAsync(byte[] bytes, System.Threading.CancellationToken cancel)
		{
			using (var outputStream = new MemoryStream())
			{
				using (var compressionStream = new GZipStream(outputStream, CompressionLevel.Optimal))
				{
					await compressionStream.WriteAsync(bytes, 0, bytes.Length, cancel);
				}
				return outputStream.ToArray();
			}
		}
	}
}

Here is an alternate version of the WeatherForecast controller's code which also works with the Blazor WASM client.

using ProjectName.Shared;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;

namespace ProjectName.Server.Controllers
{
	[ApiController]
	[Route("[controller]")]
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> logger;

		public WeatherForecastController(ILogger<WeatherForecastController> logger)
		{
			this.logger = logger;
		}

		[HttpGet]
		public async Task<IActionResult> Get(System.Threading.CancellationToken cancel) // NOTE: NOTICE THE RETURN TYPE: IActionResult
		{
			var rng = new Random();
			var data = Enumerable.Range(1, 2000).Select(index => new WeatherForecast // NOTE: NOTICE THE SIZE OF THE ARRAY
			{
				Date = DateTime.Now.AddDays(index),
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)]
			});
			var json = System.Text.Json.JsonSerializer.Serialize(data); // CONVERT DATA TO JSON
			if (!string.IsNullOrEmpty(Request.Headers["Accept-Encoding"])) // CHECK THAT THE REQUEST SUPPORTS COMPRESSION
			{
				var encodings = Request.Headers["Accept-Encoding"].ToString().ToLower().Split(',');
				for(int i = 0; i < encodings.Length; i++)
					encodings[i] = encodings[i].Trim();
				if (Array.IndexOf(encodings, "gzip") > -1) // ONLY SUPPORTING GZIP IN THIS EXAMPLE!!!
				{
					Response.Headers.Add("Content-Encoding", "gzip");
					var compressedBytes = await Compressor.CompressBytesAsync(System.Text.Encoding.UTF8.GetBytes(json), cancel);
					ReadOnlyMemory<byte> bytes = new ReadOnlyMemory<byte>(compressedBytes);
					await Response.BodyWriter.WriteAsync(bytes, cancel); // RETURN THE COMPRESSED DATA
					return Ok();
				}
			}
			Response.ContentType = "application/json"; // ADD THE CONTENT TYPE
			await Response.WriteAsync(json, cancel); // RETURN THE NON-COMPRESSED DATA
			return Ok();

		}
	}

	internal class Compressor
	{
		public static async Task<byte[]> CompressBytesAsync(byte[] bytes, System.Threading.CancellationToken cancel)
		{
			using (var outputStream = new MemoryStream())
			{
				using (var compressionStream = new GZipStream(outputStream, CompressionLevel.Optimal))
				{
					await compressionStream.WriteAsync(bytes, 0, bytes.Length, cancel);
				}
				return outputStream.ToArray();
			}
		}
	}
}

Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site.
OK