Skip to content

Commit c62092e

Browse files
CopilotJerryNixon
andauthored
Add HTTP Response Compression Support (#3003)
## Why make this change? Closes #2711 DAB does not currently support response compression (gzip/brotli), which can reduce payload sizes by ~79% for large JSON responses. This is especially beneficial for REST and GraphQL endpoints over slow or high-latency networks. ## What is this change? Adds response compression middleware with configurable compression levels. **Configuration Model:** - `CompressionLevel` enum: `optimal`, `fastest`, `none` - `CompressionOptions` record with JSON converter and validation - Default: `optimal` **Runtime Behavior:** - Gzip and Brotli providers enabled for HTTPS - Applies to REST, GraphQL, and MCP endpoints - Logs compression level on startup (silent when `none`) **Schema & CLI:** ```json { "runtime": { "compression": { "level": "optimal" } } } ``` ## How was this tested? - [ ] Integration Tests - [x] Unit Tests Added 3 tests in `ConfigureOptionsTests.cs` covering all compression levels. ## Sample Request(s) **CLI Configuration:** ```bash dab configure --runtime.compression.level fastest ``` **Sample Request/Response:** ``` GET /api/employees HTTP/1.1 Accept-Encoding: gzip, br HTTP/1.1 200 OK Content-Encoding: gzip Content-Type: application/json ``` **Startup Output (when enabled):** ``` Response compression enabled with level 'Optimal' for REST, GraphQL, and MCP endpoints. ``` <!-- START COPILOT CODING AGENT SUFFIX --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>⭐ [Enhancement]: Add Compression</issue_title> > <issue_description>Data API builder does not currently support response compression (e.g., gzip or brotli), which can significantly reduce payload size and improve performance over slow or high-latency networks. Compression is especially beneficial for large JSON responses, common in REST and GraphQL endpoints. Adding support at the server level—either via built-in middleware or integration guidance—would help DAB serve clients more efficiently. This is a common feature in modern web APIs and would be a strong addition to the roadmap. > > **Sample request** > > ``` text > GET /api/employees HTTP/1.1 > Host: api.example.com > Accept-Encoding: gzip, deflate, br > ``` > > **Sample response** > > ```text > HTTP/1.1 200 OK > Content-Encoding: gzip > Content-Type: application/json > Content-Length: 920 > ``` > > **Payload impact** > > | Payload Type | Uncompressed (bytes) | Gzipped (bytes) | Compression Savings | > |-|-|-|-| > | Large JSON array | 4,356 | 920 | ~79% | > > ## Configuration Update > > ```json > { > "runtime": { > "compression": { > "level": "fastest" | "optimal" | "none" <default: optimal> > } > } > } > ``` > > ## CLI update > > ```text > dab configure --runtime.compression.level fastest > ``` > > ## Starter JSON Schema section > > ```json > { > "runtime": { > "type": "object", > "properties": { > "compression": { > "type": "object", > "description": "Configures HTTP response compression settings.", > "properties": { > "level": { > "type": "string", > "enum": ["fastest", "optimal", "none"], > "default": "optimal", > "description": "Specifies the response compression level." > } > }, > "additionalProperties": false > } > } > } > } > ``` > > ## Basic ASPNET implementation > > ```csharp > var builder = WebApplication.CreateBuilder(args); > > builder.Services.AddResponseCompression(options => > { > options.EnableForHttps = true; > options.Providers.Add<GzipCompressionProvider>(); > }); > builder.Services.Configure<GzipCompressionProviderOptions>(options => > { > options.Level = CompressionLevel.Fastest; > }); > > var app = builder.Build(); > > app.UseResponseCompression(); > > // map endpoints and controllers > > app.Run(); > ``` > > ## FAQ > > 1. **Is this backwards compatible?** > Yes. Clients that don’t request compression (via `Accept-Encoding`) receive uncompressed responses exactly as before. > > 2. **Does this require a special client?** > No. Most modern browsers and HTTP clients (like curl, Postman, fetch, axios) already support compressed responses out-of-the-box. > > 3. **Does this impact controller logic?** > No. Compression is handled at the middleware level, completely separate from your controller logic or data access code. > > 4. **What is the compute cost compared to bandwidth savings?** > Compression uses CPU but reduces payload size dramatically. In most APIs, the tradeoff favors compression—especially for large or repetitive JSON responses. The `CompressionLevel.Fastest` option balances speed and size. > > 5. **Does this work over HTTPS?** > Yes. Response compression is fully supported over HTTPS and can be explicitly enabled with `EnableForHttps = true`. > > 6. **Can it be customized for specific MIME types?** > Yes. You can add custom MIME types (e.g., `application/graphql+json`) if needed. > > 7. **What compression formats are supported?** > Gzip is most common, but Brotli and Deflate are also supported in ASP.NET Core with the appropriate providers. > > 8. **Does it work with REST and GraphQL?** > Yes. Compression applies to all HTTP responses regardless of the endpoint type, including both REST and GraphQL APIs. > > 9. **Does Hot Chocolate support it?** > Hot Chocolate runs on ASP.NET Core and inherits its middleware pipeline, so response compression works with it automatically—no special configuration needed in Hot Chocolate itself.</issue_description> > > <agent_instructions>You are a senior microsoft engineer dedicated to terse code and changing as little code as possible in order to complete the task.</agent_instructions> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > </comments> > </details> - Fixes #2636 <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/Azure/data-api-builder/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: JerryNixon <[email protected]>
1 parent 6f62845 commit c62092e

11 files changed

Lines changed: 628 additions & 1 deletion

File tree

schemas/dab.draft.schema.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,19 @@
424424
}
425425
}
426426
},
427+
"compression": {
428+
"type": "object",
429+
"description": "Configures HTTP response compression settings.",
430+
"additionalProperties": false,
431+
"properties": {
432+
"level": {
433+
"type": "string",
434+
"enum": ["optimal", "fastest", "none"],
435+
"default": "optimal",
436+
"description": "Specifies the response compression level. 'optimal' provides best compression ratio, 'fastest' prioritizes speed, 'none' disables compression."
437+
}
438+
}
439+
},
427440
"telemetry": {
428441
"type": "object",
429442
"description": "Telemetry configuration",

src/Cli.Tests/ConfigureOptionsTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,34 @@ public void TestUpdateTTLForCacheSettings(int updatedTtlValue)
540540
Assert.AreEqual(updatedTtlValue, runtimeConfig.Runtime.Cache.TtlSeconds);
541541
}
542542

543+
/// <summary>
544+
/// Tests that running "dab configure --runtime.compression.level {value}" on a config with various values results
545+
/// in runtime config update. Takes in updated value for compression.level and
546+
/// validates whether the runtime config reflects those updated values.
547+
[DataTestMethod]
548+
[DataRow(CompressionLevel.Fastest, DisplayName = "Update Compression.Level to fastest.")]
549+
[DataRow(CompressionLevel.Optimal, DisplayName = "Update Compression.Level to optimal.")]
550+
[DataRow(CompressionLevel.None, DisplayName = "Update Compression.Level to none.")]
551+
public void TestUpdateLevelForCompressionSettings(CompressionLevel updatedLevelValue)
552+
{
553+
// Arrange -> all the setup which includes creating options.
554+
SetupFileSystemWithInitialConfig(INITIAL_CONFIG);
555+
556+
// Act: Attempts to update compression level value
557+
ConfigureOptions options = new(
558+
runtimeCompressionLevel: updatedLevelValue,
559+
config: TEST_RUNTIME_CONFIG_FILE
560+
);
561+
bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!);
562+
563+
// Assert: Validate the Level Value is updated
564+
Assert.IsTrue(isSuccess);
565+
string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
566+
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig));
567+
Assert.IsNotNull(runtimeConfig.Runtime?.Compression?.Level);
568+
Assert.AreEqual(updatedLevelValue, runtimeConfig.Runtime.Compression.Level);
569+
}
570+
543571
/// <summary>
544572
/// Tests that running "dab configure --runtime.host.mode {value}" on a config with various values results
545573
/// in runtime config update. Takes in updated value for host.mode and

src/Cli/Commands/ConfigureOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public ConfigureOptions(
4848
bool? runtimeMcpDmlToolsExecuteEntityEnabled = null,
4949
bool? runtimeCacheEnabled = null,
5050
int? runtimeCacheTtl = null,
51+
CompressionLevel? runtimeCompressionLevel = null,
5152
HostMode? runtimeHostMode = null,
5253
IEnumerable<string>? runtimeHostCorsOrigins = null,
5354
bool? runtimeHostCorsAllowCredentials = null,
@@ -105,6 +106,8 @@ public ConfigureOptions(
105106
// Cache
106107
RuntimeCacheEnabled = runtimeCacheEnabled;
107108
RuntimeCacheTTL = runtimeCacheTtl;
109+
// Compression
110+
RuntimeCompressionLevel = runtimeCompressionLevel;
108111
// Host
109112
RuntimeHostMode = runtimeHostMode;
110113
RuntimeHostCorsOrigins = runtimeHostCorsOrigins;
@@ -212,6 +215,9 @@ public ConfigureOptions(
212215
[Option("runtime.cache.ttl-seconds", Required = false, HelpText = "Customize the DAB cache's global default time to live in seconds. Default: 5 seconds (Integer).")]
213216
public int? RuntimeCacheTTL { get; }
214217

218+
[Option("runtime.compression.level", Required = false, HelpText = "Set the response compression level. Allowed values: optimal (default), fastest, none.")]
219+
public CompressionLevel? RuntimeCompressionLevel { get; }
220+
215221
[Option("runtime.host.mode", Required = false, HelpText = "Set the host running mode of DAB in Development or Production. Default: Development.")]
216222
public HostMode? RuntimeHostMode { get; }
217223

src/Cli/ConfigGenerator.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,21 @@ private static bool TryUpdateConfiguredRuntimeOptions(
849849
}
850850
}
851851

852+
// Compression: Level
853+
if (options.RuntimeCompressionLevel != null)
854+
{
855+
CompressionOptions updatedCompressionOptions = runtimeConfig?.Runtime?.Compression ?? new();
856+
bool status = TryUpdateConfiguredCompressionValues(options, ref updatedCompressionOptions);
857+
if (status)
858+
{
859+
runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { Compression = updatedCompressionOptions } };
860+
}
861+
else
862+
{
863+
return false;
864+
}
865+
}
866+
852867
// Host: Mode, Cors.Origins, Cors.AllowCredentials, Authentication.Provider, Authentication.Jwt.Audience, Authentication.Jwt.Issuer
853868
if (options.RuntimeHostMode != null ||
854869
options.RuntimeHostCorsOrigins != null ||
@@ -1226,6 +1241,37 @@ private static bool TryUpdateConfiguredCacheValues(
12261241
}
12271242
}
12281243

1244+
/// <summary>
1245+
/// Attempts to update the Config parameters in the Compression runtime settings based on the provided value.
1246+
/// Validates user-provided parameters and then returns true if the updated Compression options
1247+
/// need to be overwritten on the existing config parameters.
1248+
/// </summary>
1249+
/// <param name="options">options.</param>
1250+
/// <param name="updatedCompressionOptions">updatedCompressionOptions.</param>
1251+
/// <returns>True if the value needs to be updated in the runtime config, else false</returns>
1252+
private static bool TryUpdateConfiguredCompressionValues(
1253+
ConfigureOptions options,
1254+
ref CompressionOptions updatedCompressionOptions)
1255+
{
1256+
try
1257+
{
1258+
// Runtime.Compression.Level
1259+
CompressionLevel? updatedValue = options?.RuntimeCompressionLevel;
1260+
if (updatedValue != null)
1261+
{
1262+
updatedCompressionOptions = updatedCompressionOptions with { Level = updatedValue.Value, UserProvidedLevel = true };
1263+
_logger.LogInformation("Updated RuntimeConfig with Runtime.Compression.Level as '{updatedValue}'", updatedValue);
1264+
}
1265+
1266+
return true;
1267+
}
1268+
catch (Exception ex)
1269+
{
1270+
_logger.LogError("Failed to configure RuntimeConfig.Compression with exception message: {exceptionMessage}.", ex.Message);
1271+
return false;
1272+
}
1273+
}
1274+
12291275
/// <summary>
12301276
/// Attempts to update the Config parameters in the Host runtime settings based on the provided value.
12311277
/// Validates that any user-provided parameter value is valid and then returns true if the updated Host options
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using Azure.DataApiBuilder.Config.ObjectModel;
7+
8+
namespace Azure.DataApiBuilder.Config.Converters;
9+
10+
/// <summary>
11+
/// Defines how DAB reads and writes the compression options (JSON).
12+
/// </summary>
13+
internal class CompressionOptionsConverterFactory : JsonConverterFactory
14+
{
15+
/// <inheritdoc/>
16+
public override bool CanConvert(Type typeToConvert)
17+
{
18+
return typeToConvert.IsAssignableTo(typeof(CompressionOptions));
19+
}
20+
21+
/// <inheritdoc/>
22+
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
23+
{
24+
return new CompressionOptionsConverter();
25+
}
26+
27+
private class CompressionOptionsConverter : JsonConverter<CompressionOptions>
28+
{
29+
/// <summary>
30+
/// Defines how DAB reads the compression options and defines which values are
31+
/// used to instantiate CompressionOptions.
32+
/// </summary>
33+
public override CompressionOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
34+
{
35+
if (reader.TokenType == JsonTokenType.Null)
36+
{
37+
return null;
38+
}
39+
40+
if (reader.TokenType != JsonTokenType.StartObject)
41+
{
42+
throw new JsonException("Expected start of object.");
43+
}
44+
45+
CompressionLevel level = CompressionOptions.DEFAULT_LEVEL;
46+
bool userProvidedLevel = false;
47+
48+
while (reader.Read())
49+
{
50+
if (reader.TokenType == JsonTokenType.EndObject)
51+
{
52+
break;
53+
}
54+
55+
if (reader.TokenType == JsonTokenType.PropertyName)
56+
{
57+
string? propertyName = reader.GetString();
58+
reader.Read();
59+
60+
if (string.Equals(propertyName, "level", StringComparison.OrdinalIgnoreCase))
61+
{
62+
string? levelStr = reader.GetString();
63+
if (levelStr is not null)
64+
{
65+
if (Enum.TryParse<CompressionLevel>(levelStr, ignoreCase: true, out CompressionLevel parsedLevel))
66+
{
67+
level = parsedLevel;
68+
userProvidedLevel = true;
69+
}
70+
else
71+
{
72+
throw new JsonException($"Invalid compression level: '{levelStr}'. Valid values are: optimal, fastest, none.");
73+
}
74+
}
75+
}
76+
else
77+
{
78+
// Skip unknown properties and their values (including objects/arrays)
79+
reader.Skip();
80+
}
81+
}
82+
}
83+
84+
return new CompressionOptions(level) with { UserProvidedLevel = userProvidedLevel };
85+
}
86+
87+
/// <summary>
88+
/// When writing the CompressionOptions back to a JSON file, only write the level
89+
/// property and value when it was provided by the user.
90+
/// </summary>
91+
public override void Write(Utf8JsonWriter writer, CompressionOptions value, JsonSerializerOptions options)
92+
{
93+
writer.WriteStartObject();
94+
95+
if (value is not null && value.UserProvidedLevel)
96+
{
97+
writer.WritePropertyName("level");
98+
writer.WriteStringValue(value.Level.ToString().ToLowerInvariant());
99+
}
100+
101+
writer.WriteEndObject();
102+
}
103+
}
104+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.DataApiBuilder.Config.ObjectModel;
7+
8+
/// <summary>
9+
/// Specifies the compression level for HTTP response compression.
10+
/// </summary>
11+
[JsonConverter(typeof(JsonStringEnumConverter))]
12+
public enum CompressionLevel
13+
{
14+
/// <summary>
15+
/// Provides the best compression ratio at the cost of speed.
16+
/// </summary>
17+
Optimal,
18+
19+
/// <summary>
20+
/// Provides the fastest compression at the cost of compression ratio.
21+
/// </summary>
22+
Fastest,
23+
24+
/// <summary>
25+
/// Disables compression.
26+
/// </summary>
27+
None
28+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Azure.DataApiBuilder.Config.ObjectModel;
7+
8+
/// <summary>
9+
/// Configuration options for HTTP response compression.
10+
/// </summary>
11+
public record CompressionOptions
12+
{
13+
/// <summary>
14+
/// Default compression level is Optimal.
15+
/// </summary>
16+
public const CompressionLevel DEFAULT_LEVEL = CompressionLevel.Optimal;
17+
18+
/// <summary>
19+
/// The compression level to use for HTTP response compression.
20+
/// </summary>
21+
[JsonPropertyName("level")]
22+
public CompressionLevel Level { get; init; } = DEFAULT_LEVEL;
23+
24+
/// <summary>
25+
/// Flag which informs CLI and JSON serializer whether to write Level
26+
/// property and value to the runtime config file.
27+
/// </summary>
28+
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
29+
public bool UserProvidedLevel { get; init; } = false;
30+
31+
[JsonConstructor]
32+
public CompressionOptions(CompressionLevel Level = DEFAULT_LEVEL)
33+
{
34+
this.Level = Level;
35+
this.UserProvidedLevel = true;
36+
}
37+
38+
/// <summary>
39+
/// Default parameterless constructor for cases where no compression level is specified.
40+
/// </summary>
41+
public CompressionOptions()
42+
{
43+
this.Level = DEFAULT_LEVEL;
44+
this.UserProvidedLevel = false;
45+
}
46+
}

src/Config/ObjectModel/RuntimeOptions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public record RuntimeOptions
1717
public RuntimeCacheOptions? Cache { get; init; }
1818
public PaginationOptions? Pagination { get; init; }
1919
public RuntimeHealthCheckConfig? Health { get; init; }
20+
public CompressionOptions? Compression { get; init; }
2021

2122
[JsonConstructor]
2223
public RuntimeOptions(
@@ -28,7 +29,8 @@ public RuntimeOptions(
2829
TelemetryOptions? Telemetry = null,
2930
RuntimeCacheOptions? Cache = null,
3031
PaginationOptions? Pagination = null,
31-
RuntimeHealthCheckConfig? Health = null)
32+
RuntimeHealthCheckConfig? Health = null,
33+
CompressionOptions? Compression = null)
3234
{
3335
this.Rest = Rest;
3436
this.GraphQL = GraphQL;
@@ -39,6 +41,7 @@ public RuntimeOptions(
3941
this.Cache = Cache;
4042
this.Pagination = Pagination;
4143
this.Health = Health;
44+
this.Compression = Compression;
4245
}
4346

4447
/// <summary>

src/Config/RuntimeConfigLoader.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ public static JsonSerializerOptions GetSerializationOptions(
320320
options.Converters.Add(new EntityMcpOptionsConverterFactory());
321321
options.Converters.Add(new RuntimeCacheOptionsConverterFactory());
322322
options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory());
323+
options.Converters.Add(new CompressionOptionsConverterFactory());
323324
options.Converters.Add(new MultipleCreateOptionsConverter());
324325
options.Converters.Add(new MultipleMutationOptionsConverter(options));
325326
options.Converters.Add(new DataSourceConverterFactory(replacementSettings));

0 commit comments

Comments
 (0)