articles » current » asp-net-core » compression-rest-api-output

ASP.NET Core: Compress REST API Output

Compress the output from a Web API to a greater level than if enabling site-wide compression

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

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

This article shows how to enable more than one compression algorithm for the Web API. The rest of the site remains uncompressed. 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 then the GZipStream second and sending the compressed data to the client as a File.

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 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 COMPRESSED DATA AS A FILE
	}
	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 COMPRESSED DATA AS A FILE
	}
}
Response.ContentType = "application/json"; // ADD THE CONTENT TYPE
return Content(json); // RETURN 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 JavaScript client (using either window.fetch() or XMLHttpRequest()).

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)
				{
					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)
				{
					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();
			}
		}
	}
}

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.
CLOSE