PROWAREtech

articles » current » blazor » wasm » compress-rest-api-output

Blazor: Compress Web API or REST API Output

Example of using the compression classes, such as the BrotliStream class, to compress outgoing data of a Web API or REST API to achieve greater compression ratios than what is done with site-wide compression enabled.

This example uses ASP.NET Core 3.1. It should be compatible with later versions of .NET including .NET 5, .NET 6 and .NET 8. 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 reason to do it this way as opposed to site-wide is because the compression ratios are much better so if using a mobile device with a slow connection then this will be a good option. See the level of compression that can be achieved by Brotli vs what the server middle-ware achieves at its "fastest" setting. If compressing static files then setting up a controller to compress the files saving them in an alternate folder and serving them from this folder thereby reducing the number of times that the data are compressed would be a wise decision. Also, the most aggresive compression algorithms can be used with this scenario.

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 (and the DeflateStream third) if the client does not accept Brotli. Then the compressed data is sent to the client.

Here is the snippet 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(',', StringSplitOptions.TrimEntries);
	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(',', StringSplitOptions.TrimEntries);
				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();
			}
		}
	}
}

PROWAREtech

Hello there! How can I help you today?
Ask any question

PROWAREtech

This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
ACCEPT REJECT