Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,19 @@
}
}
},
"compression": {
"type": "object",
"description": "Configures HTTP response compression settings.",
"additionalProperties": false,
"properties": {
"level": {
"type": "string",
"enum": ["optimal", "fastest", "none"],
"default": "optimal",
"description": "Specifies the response compression level. 'optimal' provides best compression ratio, 'fastest' prioritizes speed, 'none' disables compression."
}
}
},
"telemetry": {
"type": "object",
"description": "Telemetry configuration",
Expand Down
28 changes: 28 additions & 0 deletions src/Cli.Tests/ConfigureOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,34 @@ public void TestUpdateTTLForCacheSettings(int updatedTtlValue)
Assert.AreEqual(updatedTtlValue, runtimeConfig.Runtime.Cache.TtlSeconds);
}

/// <summary>
/// Tests that running "dab configure --runtime.compression.level {value}" on a config with various values results
/// in runtime config update. Takes in updated value for compression.level and
/// validates whether the runtime config reflects those updated values
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing closing period in the documentation comment. The summary should end with a period for consistency with other test documentation in the file.

Suggested change
/// validates whether the runtime config reflects those updated values
/// validates whether the runtime config reflects those updated values.

Copilot uses AI. Check for mistakes.
[DataTestMethod]
[DataRow(CompressionLevel.Fastest, DisplayName = "Update Compression.Level to fastest.")]
[DataRow(CompressionLevel.Optimal, DisplayName = "Update Compression.Level to optimal.")]
[DataRow(CompressionLevel.None, DisplayName = "Update Compression.Level to none.")]
public void TestUpdateLevelForCompressionSettings(CompressionLevel updatedLevelValue)
{
// Arrange -> all the setup which includes creating options.
SetupFileSystemWithInitialConfig(INITIAL_CONFIG);

// Act: Attempts to update compression level value
ConfigureOptions options = new(
runtimeCompressionLevel: updatedLevelValue,
config: TEST_RUNTIME_CONFIG_FILE
);
bool isSuccess = TryConfigureSettings(options, _runtimeConfigLoader!, _fileSystem!);

// Assert: Validate the Level Value is updated
Assert.IsTrue(isSuccess);
string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE);
Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig));
Assert.IsNotNull(runtimeConfig.Runtime?.Compression?.Level);
Assert.AreEqual(updatedLevelValue, runtimeConfig.Runtime.Compression.Level);
}

/// <summary>
/// Tests that running "dab configure --runtime.host.mode {value}" on a config with various values results
/// in runtime config update. Takes in updated value for host.mode and
Expand Down
6 changes: 6 additions & 0 deletions src/Cli/Commands/ConfigureOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public ConfigureOptions(
bool? runtimeMcpDmlToolsExecuteEntityEnabled = null,
bool? runtimeCacheEnabled = null,
int? runtimeCacheTtl = null,
CompressionLevel? runtimeCompressionLevel = null,
HostMode? runtimeHostMode = null,
IEnumerable<string>? runtimeHostCorsOrigins = null,
bool? runtimeHostCorsAllowCredentials = null,
Expand Down Expand Up @@ -103,6 +104,8 @@ public ConfigureOptions(
// Cache
RuntimeCacheEnabled = runtimeCacheEnabled;
RuntimeCacheTTL = runtimeCacheTtl;
// Compression
RuntimeCompressionLevel = runtimeCompressionLevel;
// Host
RuntimeHostMode = runtimeHostMode;
RuntimeHostCorsOrigins = runtimeHostCorsOrigins;
Expand Down Expand Up @@ -207,6 +210,9 @@ public ConfigureOptions(
[Option("runtime.cache.ttl-seconds", Required = false, HelpText = "Customize the DAB cache's global default time to live in seconds. Default: 5 seconds (Integer).")]
public int? RuntimeCacheTTL { get; }

[Option("runtime.compression.level", Required = false, HelpText = "Set the response compression level. Allowed values: optimal (default), fastest, none.")]
public CompressionLevel? RuntimeCompressionLevel { get; }

[Option("runtime.host.mode", Required = false, HelpText = "Set the host running mode of DAB in Development or Production. Default: Development.")]
public HostMode? RuntimeHostMode { get; }

Expand Down
46 changes: 46 additions & 0 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,21 @@ private static bool TryUpdateConfiguredRuntimeOptions(
}
}

// Compression: Level
if (options.RuntimeCompressionLevel != null)
{
CompressionOptions updatedCompressionOptions = runtimeConfig?.Runtime?.Compression ?? new();
bool status = TryUpdateConfiguredCompressionValues(options, ref updatedCompressionOptions);
if (status)
{
runtimeConfig = runtimeConfig! with { Runtime = runtimeConfig.Runtime! with { Compression = updatedCompressionOptions } };
}
else
{
return false;
}
}

// Host: Mode, Cors.Origins, Cors.AllowCredentials, Authentication.Provider, Authentication.Jwt.Audience, Authentication.Jwt.Issuer
if (options.RuntimeHostMode != null ||
options.RuntimeHostCorsOrigins != null ||
Expand Down Expand Up @@ -1197,6 +1212,37 @@ private static bool TryUpdateConfiguredCacheValues(
}
}

/// <summary>
/// Attempts to update the Config parameters in the Compression runtime settings based on the provided value.
/// Validates user-provided parameters and then returns true if the updated Compression options
/// need to be overwritten on the existing config parameters.
/// </summary>
/// <param name="options">options.</param>
/// <param name="updatedCompressionOptions">updatedCompressionOptions.</param>
/// <returns>True if the value needs to be updated in the runtime config, else false</returns>
private static bool TryUpdateConfiguredCompressionValues(
ConfigureOptions options,
ref CompressionOptions updatedCompressionOptions)
{
try
{
// Runtime.Compression.Level
CompressionLevel? updatedValue = options?.RuntimeCompressionLevel;
if (updatedValue != null)
{
updatedCompressionOptions = updatedCompressionOptions with { Level = updatedValue.Value, UserProvidedLevel = true };
_logger.LogInformation("Updated RuntimeConfig with Runtime.Compression.Level as '{updatedValue}'", updatedValue);
}

return true;
}
catch (Exception ex)
{
_logger.LogError("Failed to update RuntimeConfig.Compression with exception message: {exceptionMessage}.", ex.Message);
return false;
}
}

/// <summary>
/// Attempts to update the Config parameters in the Host runtime settings based on the provided value.
/// Validates that any user-provided parameter value is valid and then returns true if the updated Host options
Expand Down
99 changes: 99 additions & 0 deletions src/Config/Converters/CompressionOptionsConverterFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;
using Azure.DataApiBuilder.Config.ObjectModel;

namespace Azure.DataApiBuilder.Config.Converters;

/// <summary>
/// Defines how DAB reads and writes the compression options (JSON).
/// </summary>
internal class CompressionOptionsConverterFactory : JsonConverterFactory
{
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsAssignableTo(typeof(CompressionOptions));
}

/// <inheritdoc/>
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return new CompressionOptionsConverter();
}

private class CompressionOptionsConverter : JsonConverter<CompressionOptions>
{
/// <summary>
/// Defines how DAB reads the compression options and defines which values are
/// used to instantiate CompressionOptions.
/// </summary>
public override CompressionOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}

if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("Expected start of object.");
}

CompressionLevel level = CompressionOptions.DEFAULT_LEVEL;
bool userProvidedLevel = false;

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
break;
}

if (reader.TokenType == JsonTokenType.PropertyName)
{
string? propertyName = reader.GetString();
reader.Read();

if (string.Equals(propertyName, "level", StringComparison.OrdinalIgnoreCase))
{
string? levelStr = reader.GetString();
if (levelStr is not null)
{
if (Enum.TryParse<CompressionLevel>(levelStr, ignoreCase: true, out CompressionLevel parsedLevel))
{
level = parsedLevel;
userProvidedLevel = true;
}
else
{
throw new JsonException($"Invalid compression level: '{levelStr}'. Valid values are: optimal, fastest, none.");
}
}
}
}
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The converter doesn't properly handle unknown properties with complex values (objects or arrays). After reading a property name on line 58, if the property is not "level", the code continues the loop without skipping the property value. This works for simple values but could cause parsing issues if an unknown property contains an object or array.

Add a default case to properly skip unknown properties:

if (string.Equals(propertyName, "level", StringComparison.OrdinalIgnoreCase))
{
    string? levelStr = reader.GetString();
    if (levelStr is not null)
    {
        if (Enum.TryParse<CompressionLevel>(levelStr, ignoreCase: true, out CompressionLevel parsedLevel))
        {
            level = parsedLevel;
            userProvidedLevel = true;
        }
        else
        {
            throw new JsonException($"Invalid compression level: '{levelStr}'. Valid values are: optimal, fastest, none.");
        }
    }
}
else
{
    // Skip unknown properties
    reader.Skip();
}
Suggested change
}
else
{
// Skip unknown properties and their values (including objects/arrays)
reader.Skip();
}

Copilot uses AI. Check for mistakes.
}

return new CompressionOptions(level) with { UserProvidedLevel = userProvidedLevel };
}

/// <summary>
/// When writing the CompressionOptions back to a JSON file, only write the level
/// property and value when it was provided by the user.
/// </summary>
public override void Write(Utf8JsonWriter writer, CompressionOptions value, JsonSerializerOptions options)
{
writer.WriteStartObject();

if (value is not null && value.UserProvidedLevel)
{
writer.WritePropertyName("level");
writer.WriteStringValue(value.Level.ToString().ToLowerInvariant());
}

writer.WriteEndObject();
}
}
}
28 changes: 28 additions & 0 deletions src/Config/ObjectModel/CompressionLevel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;

/// <summary>
/// Specifies the compression level for HTTP response compression.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum CompressionLevel
{
/// <summary>
/// Provides the best compression ratio at the cost of speed.
/// </summary>
Optimal,

/// <summary>
/// Provides the fastest compression at the cost of compression ratio.
/// </summary>
Fastest,

/// <summary>
/// Disables compression.
/// </summary>
None
}
46 changes: 46 additions & 0 deletions src/Config/ObjectModel/CompressionOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;

/// <summary>
/// Configuration options for HTTP response compression.
/// </summary>
public record CompressionOptions
{
/// <summary>
/// Default compression level is Optimal.
/// </summary>
public const CompressionLevel DEFAULT_LEVEL = CompressionLevel.Optimal;

/// <summary>
/// The compression level to use for HTTP response compression.
/// </summary>
[JsonPropertyName("level")]
public CompressionLevel Level { get; init; } = DEFAULT_LEVEL;

/// <summary>
/// Flag which informs CLI and JSON serializer whether to write Level
/// property and value to the runtime config file.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.Always)]
public bool UserProvidedLevel { get; init; } = false;

[JsonConstructor]
public CompressionOptions(CompressionLevel Level = DEFAULT_LEVEL)
{
this.Level = Level;
this.UserProvidedLevel = true;
}

/// <summary>
/// Default parameterless constructor for cases where no compression level is specified.
/// </summary>
public CompressionOptions()
{
this.Level = DEFAULT_LEVEL;
this.UserProvidedLevel = false;
}
}
5 changes: 4 additions & 1 deletion src/Config/ObjectModel/RuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public record RuntimeOptions
public RuntimeCacheOptions? Cache { get; init; }
public PaginationOptions? Pagination { get; init; }
public RuntimeHealthCheckConfig? Health { get; init; }
public CompressionOptions? Compression { get; init; }

[JsonConstructor]
public RuntimeOptions(
Expand All @@ -28,7 +29,8 @@ public RuntimeOptions(
TelemetryOptions? Telemetry = null,
RuntimeCacheOptions? Cache = null,
PaginationOptions? Pagination = null,
RuntimeHealthCheckConfig? Health = null)
RuntimeHealthCheckConfig? Health = null,
CompressionOptions? Compression = null)
{
this.Rest = Rest;
this.GraphQL = GraphQL;
Expand All @@ -39,6 +41,7 @@ public RuntimeOptions(
this.Cache = Cache;
this.Pagination = Pagination;
this.Health = Health;
this.Compression = Compression;
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ public static JsonSerializerOptions GetSerializationOptions(
options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings));
options.Converters.Add(new RuntimeCacheOptionsConverterFactory());
options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory());
options.Converters.Add(new CompressionOptionsConverterFactory());
options.Converters.Add(new MultipleCreateOptionsConverter());
options.Converters.Add(new MultipleMutationOptionsConverter(options));
options.Converters.Add(new DataSourceConverterFactory(replacementSettings));
Expand Down
Loading