From 2fc665db36699c9bbf9d14c56f8a0d72a80cf980 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 20 Jun 2025 15:55:16 -0700 Subject: [PATCH 01/37] allow unused cache-control options without error --- .../Resolvers/Sql Query Structures/SqlQueryStructure.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 1ce54cbd76..cedb98a305 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -587,15 +587,6 @@ private void AddCacheControlOptions(IHeaderDictionary? httpRequestHeaders) { CacheControlOption = cacheControlOption; } - - if (!string.IsNullOrEmpty(CacheControlOption) && - !cacheControlHeaderOptions.Contains(CacheControlOption)) - { - throw new DataApiBuilderException( - message: "Request Header Cache-Control is invalid: " + CacheControlOption, - statusCode: HttpStatusCode.BadRequest, - subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest); - } } /// From cfdc2b5e0a7322b1fc09a436754cd005dd12d013 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 11 Sep 2025 08:14:36 -0700 Subject: [PATCH 02/37] extend variable replacment and include AKV --- src/Cli.Tests/EnvironmentTests.cs | 7 +- src/Config/Azure.DataApiBuilder.Config.csproj | 1 + .../AzureKeyVaultOptionsConverterFactory.cs | 93 +++++++++ .../Converters/StringJsonConverterFactory.cs | 73 ++----- .../Converters/Utf8JsonReaderExtensions.cs | 7 +- ...erializationVariableReplacementSettings.cs | 192 ++++++++++++++++++ src/Config/FileSystemRuntimeConfigLoader.cs | 2 +- src/Config/ObjectModel/RuntimeConfig.cs | 2 +- src/Config/RuntimeConfigLoader.cs | 124 +++++++++-- src/Directory.Packages.props | 3 +- ...untimeConfigLoaderJsonDeserializerTests.cs | 6 +- 11 files changed, 430 insertions(+), 80 deletions(-) create mode 100644 src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs create mode 100644 src/Config/DeserializationVariableReplacementSettings.cs diff --git a/src/Cli.Tests/EnvironmentTests.cs b/src/Cli.Tests/EnvironmentTests.cs index 151d5babb2..02009db283 100644 --- a/src/Cli.Tests/EnvironmentTests.cs +++ b/src/Cli.Tests/EnvironmentTests.cs @@ -19,7 +19,12 @@ public class EnvironmentTests [TestInitialize] public void TestInitialize() { - StringJsonConverterFactory converterFactory = new(EnvironmentVariableReplacementFailureMode.Throw); + DeserializationVariableReplacementSettings replacementSettings = new( + doReplaceEnvVar: true, + doReplaceAKVVar: false, + envFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + + StringJsonConverterFactory converterFactory = new(replacementSettings); _options = new() { PropertyNameCaseInsensitive = true diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index a494bc38ae..6b5bdf0955 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs new file mode 100644 index 0000000000..96923e914e --- /dev/null +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -0,0 +1,93 @@ +// 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; + +/// +/// Converter factory for AzureKeyVaultOptions that does not perform variable replacement. +/// This ensures we can read the raw AKV configuration needed to set up variable replacement. +/// +internal class AzureKeyVaultOptionsConverterFactory : JsonConverterFactory +{ + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(AzureKeyVaultOptions)); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new AzureKeyVaultOptionsConverter(); + } + + private class AzureKeyVaultOptionsConverter : JsonConverter + { + /// + /// Reads AzureKeyVaultOptions without performing variable replacement. + /// Variable replacement will be handled in subsequent passes. + /// + public override AzureKeyVaultOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + string? endpoint = null; + AKVRetryPolicyOptions? retryPolicy = null; + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + return new AzureKeyVaultOptions + { + Endpoint = endpoint, + RetryPolicy = retryPolicy + }; + } + + string? property = reader.GetString(); + reader.Read(); + + switch (property) + { + case "endpoint": + if (reader.TokenType is JsonTokenType.String) + { + endpoint = reader.GetString(); + } + + break; + + case "retry-policy": + if (reader.TokenType is JsonTokenType.StartObject) + { + // Use the existing AKVRetryPolicyOptionsConverter without variable replacement + retryPolicy = JsonSerializer.Deserialize(ref reader, options); + } + + break; + + default: + reader.Skip(); + break; + } + } + } + else if (reader.TokenType is JsonTokenType.Null) + { + return null; + } + + throw new JsonException("Invalid AzureKeyVaultOptions format"); + } + + public override void Write(Utf8JsonWriter writer, AzureKeyVaultOptions value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/src/Config/Converters/StringJsonConverterFactory.cs b/src/Config/Converters/StringJsonConverterFactory.cs index 078b611789..2e581ce55e 100644 --- a/src/Config/Converters/StringJsonConverterFactory.cs +++ b/src/Config/Converters/StringJsonConverterFactory.cs @@ -4,21 +4,20 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using Azure.DataApiBuilder.Service.Exceptions; namespace Azure.DataApiBuilder.Config.Converters; /// -/// Custom string json converter factory to replace environment variables of the pattern -/// @env('ENV_NAME') with their value during deserialization. +/// Custom string json converter factory to replace environment variables and other variable patterns +/// during deserialization using the DeserializationVariableReplacementSettings. /// public class StringJsonConverterFactory : JsonConverterFactory { - private EnvironmentVariableReplacementFailureMode _replacementFailureMode; + private DeserializationVariableReplacementSettings _replacementSettings; - public StringJsonConverterFactory(EnvironmentVariableReplacementFailureMode replacementFailureMode) + public StringJsonConverterFactory(DeserializationVariableReplacementSettings replacementSettings) { - _replacementFailureMode = replacementFailureMode; + _replacementSettings = replacementSettings; } public override bool CanConvert(Type typeToConvert) @@ -28,32 +27,16 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new StringJsonConverter(_replacementFailureMode); + return new StringJsonConverter(_replacementSettings); } class StringJsonConverter : JsonConverter { - // @env\(' : match @env(' - // .*? : lazy match any character except newline 0 or more times - // (?='\)) : look ahead for ') which will combine with our lazy match - // ie: in @env('hello')goodbye') we match @env('hello') - // '\) : consume the ') into the match (look ahead doesn't capture) - // This pattern lazy matches any string that starts with @env(' and ends with ') - // ie: fooBAR@env('hello-world')bash)FOO') match: @env('hello-world') - // This matching pattern allows for the @env('') to be safely nested - // within strings that contain ') after our match. - // ie: if the environment variable "Baz" has the value of "Bar" - // fooBarBaz: "('foo@env('Baz')Baz')" would parse into - // fooBarBaz: "('fooBarBaz')" - // Note that there is no escape character currently for ') to exist - // within the name of the environment variable, but that ') is not - // a valid environment variable name in certain shells. - const string ENV_PATTERN = @"@env\('.*?(?='\))'\)"; - private EnvironmentVariableReplacementFailureMode _replacementFailureMode; + private DeserializationVariableReplacementSettings _replacementSettings; - public StringJsonConverter(EnvironmentVariableReplacementFailureMode replacementFailureMode) + public StringJsonConverter(DeserializationVariableReplacementSettings replacementSettings) { - _replacementFailureMode = replacementFailureMode; + _replacementSettings = replacementSettings; } public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -61,7 +44,18 @@ public StringJsonConverter(EnvironmentVariableReplacementFailureMode replacement if (reader.TokenType == JsonTokenType.String) { string? value = reader.GetString(); - return Regex.Replace(value!, ENV_PATTERN, new MatchEvaluator(ReplaceMatchWithEnvVariable)); + if (string.IsNullOrEmpty(value)) + { + return value; + } + + // Apply all replacement strategies configured in the settings + foreach (KeyValuePair> strategy in _replacementSettings.ReplacementStrategies) + { + value = strategy.Key.Replace(value, new MatchEvaluator(strategy.Value)); + } + + return value; } if (reader.TokenType == JsonTokenType.Null) @@ -76,30 +70,5 @@ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOp { writer.WriteStringValue(value); } - - private string ReplaceMatchWithEnvVariable(Match match) - { - // [^@env\(] : any substring that is not @env( - // .* : any char except newline any number of times - // (?=\)) : look ahead for end char of ) - // This pattern greedy matches all characters that are not a part of @env() - // ie: @env('hello@env('goodbye')world') match: 'hello@env('goodbye')world' - string innerPattern = @"[^@env\(].*(?=\))"; - - // strips first and last characters, ie: '''hello'' --> ''hello' - string envName = Regex.Match(match.Value, innerPattern).Value[1..^1]; - string? envValue = Environment.GetEnvironmentVariable(envName); - if (_replacementFailureMode == EnvironmentVariableReplacementFailureMode.Throw) - { - return envValue is not null ? envValue : - throw new DataApiBuilderException(message: $"Environmental Variable, {envName}, not found.", - statusCode: System.Net.HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } - else - { - return envValue ?? match.Value; - } - } } } diff --git a/src/Config/Converters/Utf8JsonReaderExtensions.cs b/src/Config/Converters/Utf8JsonReaderExtensions.cs index 20c6821d02..acb7c39f5e 100644 --- a/src/Config/Converters/Utf8JsonReaderExtensions.cs +++ b/src/Config/Converters/Utf8JsonReaderExtensions.cs @@ -36,7 +36,12 @@ static internal class Utf8JsonReaderExtensions JsonSerializerOptions options = new(); if (replaceEnvVar) { - options.Converters.Add(new StringJsonConverterFactory(replacementFailureMode)); + // Create a simple replacement settings for environment variables only + DeserializationVariableReplacementSettings replacementSettings = new( + doReplaceEnvVar: true, + doReplaceAKVVar: false, + envFailureMode: replacementFailureMode); + options.Converters.Add(new StringJsonConverterFactory(replacementSettings)); } return JsonSerializer.Deserialize(ref reader, options); diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs new file mode 100644 index 0000000000..60c03a51f3 --- /dev/null +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.RegularExpressions; +using Azure.Core; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +using Azure.DataApiBuilder.Config.Converters; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Service.Exceptions; + +namespace Azure.DataApiBuilder.Config +{ + public class DeserializationVariableReplacementSettings + { + public bool DoReplaceEnvVar { get; set; } = true; + public bool DoReplaceAKVVar { get; set; } = true; + public EnvironmentVariableReplacementFailureMode EnvFailureMode { get; set; } = EnvironmentVariableReplacementFailureMode.Throw; + + // @env\(' : match @env(' + // @AKV\(' : match @AKV(' + // .*? : lazy match any character except newline 0 or more times + // (?='\)) : look ahead for ')' which will combine with our lazy match + // ie: in @env('hello')goodbye') we match @env('hello') + // '\) : consume the ') into the match (look ahead doesn't capture) + // This pattern lazy matches any string that starts with @env(' and ends with ') + // ie: fooBAR@env('hello-world')bash)FOO') match: @env('hello-world') + // This matching pattern allows for the @env('') to be safely nested + // within strings that contain ') after our match. + // ie: if the environment variable "Baz" has the value of "Bar" + // fooBarBaz: "('foo@env('Baz')Baz')" would parse into + // fooBarBaz: "('fooBarBaz')" + // Note that there is no escape character currently for ') to exist + // within the name of the environment variable, but that ') is not + // a valid environment variable name in certain shells. + public const string OUTER_ENV_PATTERN = @"@env\('.*?(?='\))'\)"; + public const string OUTER_AKV_PATTERN = @"@AKV\('.*?(?='\))'\)"; + + // [^@env\(] : any substring that is not @env( + // [^@AKV\(] : any substring that is not @AKV( + // .* : any char except newline any number of times + // (?=\)) : look ahead for end char of ) + // This pattern greedy matches all characters that are not a part of @env() + // ie: @env('hello@env('goodbye')world') match: 'hello@env('goodbye')world' + public const string INNER_ENV_PATTERN = @"[^@env\(].*(?=\))"; + public const string INNER_AKV_PATTERN = @"[^@AKV\(].*(?=\))"; + + private AzureKeyVaultOptions? _azureKeyVaultOptions; + private readonly SecretClient? _akvClient; + + public Dictionary> ReplacementStrategies { get; private set; } = new(); + + public DeserializationVariableReplacementSettings( + bool doReplaceEnvVar = true, + bool doReplaceAKVVar = true, + EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + { + DoReplaceEnvVar = doReplaceEnvVar; + DoReplaceAKVVar = doReplaceAKVVar; + EnvFailureMode = envFailureMode; + + if (DoReplaceEnvVar) + { + ReplacementStrategies.Add( + new Regex(INNER_ENV_PATTERN, RegexOptions.Compiled), + ReplaceEnvVariable); + } + + if (DoReplaceAKVVar && _azureKeyVaultOptions is not null) + { + _akvClient = CreateSecretClient(_azureKeyVaultOptions); + ReplacementStrategies.Add( + new Regex(INNER_AKV_PATTERN, RegexOptions.Compiled), + ReplaceAKVVariable); + } + } + + public DeserializationVariableReplacementSettings( + AzureKeyVaultOptions azureKeyVaultOptions, + bool doReplaceEnvVar = true, + bool doReplaceAKVVar = true, + EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + { + _azureKeyVaultOptions = azureKeyVaultOptions; + DoReplaceEnvVar = doReplaceEnvVar; + DoReplaceAKVVar = doReplaceAKVVar; + EnvFailureMode = envFailureMode; + + if (DoReplaceEnvVar) + { + ReplacementStrategies.Add( + new Regex(INNER_ENV_PATTERN, RegexOptions.Compiled), + ReplaceEnvVariable); + } + + if (DoReplaceAKVVar && _azureKeyVaultOptions is not null) + { + _akvClient = CreateSecretClient(_azureKeyVaultOptions); + ReplacementStrategies.Add( + new Regex(INNER_AKV_PATTERN, RegexOptions.Compiled), + ReplaceAKVVariable); + } + } + + private string ReplaceEnvVariable(Match match) + { + // strips first and last characters, ie: '''hello'' --> ''hello' + string name = Regex.Match(match.Value, INNER_ENV_PATTERN).Value[1..^1]; + string? value = Environment.GetEnvironmentVariable(name); + if (EnvFailureMode is EnvironmentVariableReplacementFailureMode.Throw) + { + return value is not null ? value : + throw new DataApiBuilderException( + message: $"Environmental Variable, {name}, not found.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + return value ?? match.Value; + } + } + + private string ReplaceAKVVariable(Match match) + { + // strips first and last characters, ie: '''hello'' --> ''hello' + string name = Regex.Match(match.Value, INNER_AKV_PATTERN).Value[1..^1]; + string? value = GetAKVVariable(name); + if (EnvFailureMode == EnvironmentVariableReplacementFailureMode.Throw) + { + return value is not null ? value : + throw new DataApiBuilderException(message: $"Azure Key Vault Variable, {name}, not found.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + return value ?? match.Value; + } + } + + private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) + { + if (string.IsNullOrWhiteSpace(options.Endpoint)) + { + throw new DataApiBuilderException( + "Azure Key Vault endpoint must be specified.", + System.Net.HttpStatusCode.InternalServerError, + DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + SecretClientOptions clientOptions = new(); + + if (options.RetryPolicy is not null) + { + // Convert AKVRetryPolicyMode to RetryMode + RetryMode retryMode = options.RetryPolicy.Mode switch + { + AKVRetryPolicyMode.Fixed => RetryMode.Fixed, + AKVRetryPolicyMode.Exponential => RetryMode.Exponential, + null => RetryMode.Exponential, + _ => RetryMode.Exponential + }; + + clientOptions.Retry.Mode = retryMode; + clientOptions.Retry.MaxRetries = options.RetryPolicy.MaxCount ?? 3; + clientOptions.Retry.Delay = TimeSpan.FromSeconds(options.RetryPolicy.DelaySeconds ?? 1); + clientOptions.Retry.MaxDelay = TimeSpan.FromSeconds(options.RetryPolicy.MaxDelaySeconds ?? 16); + clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(options.RetryPolicy.NetworkTimeoutSeconds ?? 30); + } + + return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions); + } + + private string? GetAKVVariable(string name) + { + if (_akvClient is null) + { + throw new InvalidOperationException("Azure Key Vault client is not initialized."); + } + + try + { + return _akvClient.GetSecret(name).Value.Value; + } + catch (Azure.RequestFailedException ex) when (ex.Status == 404) + { + return null; + } + } + } +} diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 9c2a8e50b5..0cf5828030 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -467,7 +467,7 @@ public override string GetPublishedDraftSchemaLink() string? schemaPath = _fileSystem.Path.Combine(assemblyDirectory, "dab.draft.schema.json"); string schemaFileContent = _fileSystem.File.ReadAllText(schemaPath); - Dictionary? jsonDictionary = JsonSerializer.Deserialize>(schemaFileContent, GetSerializationOptions()); + Dictionary? jsonDictionary = JsonSerializer.Deserialize>(schemaFileContent, GetSerializationOptions(replacementSettings: null)); if (jsonDictionary is null) { diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 7cf8159952..2bc35cd92a 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -420,7 +420,7 @@ public bool CheckDataSourceExists(string dataSourceName) public string ToJson(JsonSerializerOptions? jsonSerializerOptions = null) { // get default serializer options if none provided. - jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(); + jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(replacementSettings: null); return JsonSerializer.Serialize(this, jsonSerializerOptions); } diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 4a220af0ea..b2e5560c24 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -129,6 +129,41 @@ protected void SignalConfigChanged(string message = "") /// public abstract string GetPublishedDraftSchemaLink(); + /// + /// Extracts AzureKeyVaultOptions from JSON string without performing variable replacement. + /// This is needed to get the AKV configuration for setting up variable replacement. + /// + /// JSON that represents the config file. + /// AzureKeyVaultOptions if present, null otherwise. + private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions(string json) + { + JsonSerializerOptions options = new() + { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = new HyphenatedNamingPolicy(), + ReadCommentHandling = JsonCommentHandling.Skip + }; + options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory()); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar: false)); + + try + { + using JsonDocument doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("azure-key-vault", out JsonElement akvElement)) + { + return JsonSerializer.Deserialize(akvElement.GetRawText(), options); + } + } + catch + { + // If we can't extract AKV options, return null and proceed without AKV variable replacement + return null; + } + + return null; + } + /// /// Parses a JSON string into a RuntimeConfig object for single database scenario. /// @@ -145,9 +180,37 @@ public static bool TryParseConfig(string json, ILogger? logger = null, string? connectionString = null, bool replaceEnvVar = false, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) { - JsonSerializerOptions options = GetSerializationOptions(replaceEnvVar, replacementFailureMode); + // First pass: extract AzureKeyVault options without variable replacement + AzureKeyVaultOptions? azureKeyVaultOptions = null; + if (replaceEnvVar) + { + azureKeyVaultOptions = ExtractAzureKeyVaultOptions(json); + } + + // Create replacement settings based on extracted AKV options + DeserializationVariableReplacementSettings? replacementSettings = null; + if (replaceEnvVar) + { + if (azureKeyVaultOptions is not null) + { + replacementSettings = new DeserializationVariableReplacementSettings( + azureKeyVaultOptions, + doReplaceEnvVar: true, + doReplaceAKVVar: true, + envFailureMode: replacementFailureMode); + } + else + { + replacementSettings = new DeserializationVariableReplacementSettings( + doReplaceEnvVar: true, + doReplaceAKVVar: false, + envFailureMode: replacementFailureMode); + } + } + + JsonSerializerOptions options = GetSerializationOptions(replacementSettings); try { @@ -227,9 +290,30 @@ ex is JsonException || /// /// Whether to replace environment variable with value or not while deserializing. /// By default, no replacement happens. + /// Determines failure mode for env variable replacement. public static JsonSerializerOptions GetSerializationOptions( bool replaceEnvVar = false, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) + { + DeserializationVariableReplacementSettings? replacementSettings = null; + if (replaceEnvVar) + { + replacementSettings = new DeserializationVariableReplacementSettings( + doReplaceEnvVar: true, + doReplaceAKVVar: false, // No AKV replacement without explicit AKV options + envFailureMode: replacementFailureMode); + } + + return GetSerializationOptions(replacementSettings); + } + + /// + /// Get Serializer options for the config file. + /// + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public static JsonSerializerOptions GetSerializationOptions( + DeserializationVariableReplacementSettings? replacementSettings = null) { JsonSerializerOptions options = new() { @@ -241,31 +325,35 @@ public static JsonSerializerOptions GetSerializationOptions( Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); - options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replaceEnvVar)); - options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replaceEnvVar)); + options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replacementSettings?.DoReplaceEnvVar ?? false)); options.Converters.Add(new EntityHealthOptionsConvertorFactory()); options.Converters.Add(new RestRuntimeOptionsConverterFactory()); - options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntitySourceConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntityRestOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new EntitySourceConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new EntityRestOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); - options.Converters.Add(new EntityCacheOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); options.Converters.Add(new MultipleCreateOptionsConverter()); options.Converters.Add(new MultipleMutationOptionsConverter(options)); - options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar)); + options.Converters.Add(new DataSourceConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); options.Converters.Add(new HostOptionsConvertorFactory()); - options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replaceEnvVar)); - options.Converters.Add(new FileSinkConverter(replaceEnvVar)); - - if (replaceEnvVar) + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new FileSinkConverter(replacementSettings?.DoReplaceEnvVar ?? false)); + + // Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory()); + + // Only add the extensible string converter if we have replacement settings + if (replacementSettings is not null) { - options.Converters.Add(new StringJsonConverterFactory(replacementFailureMode)); + options.Converters.Add(new StringJsonConverterFactory(replacementSettings)); } return options; diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index da600c9f63..e1cd799945 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,6 +5,7 @@ + @@ -78,4 +79,4 @@ - \ No newline at end of file + diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 8d7dae0541..4be71adc5e 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -10,7 +10,6 @@ using System.Text; using System.Text.Json; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -302,10 +301,7 @@ public void CheckConfigEnvParsingThrowExceptions(string invalidEnvVarName) { string json = @"{ ""foo"" : ""@env('envVarName'), @env('" + invalidEnvVarName + @"')"" }"; SetEnvVariables(); - StringJsonConverterFactory stringConverterFactory = new(EnvironmentVariableReplacementFailureMode.Throw); - JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; - options.Converters.Add(stringConverterFactory); - Assert.ThrowsException(() => JsonSerializer.Deserialize(json, options)); + Assert.ThrowsException(() => JsonSerializer.Deserialize(json)); } [DataRow("\"notsupporteddb\"", "", From 5eb9f0e0598c76a264e2dde2d6057c6639758266 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 12 Sep 2025 21:57:55 -0700 Subject: [PATCH 03/37] change some logic for first pass var replacement --- .../AzureKeyVaultOptionsConverterFactory.cs | 34 +++++++++++++++---- src/Config/RuntimeConfigLoader.cs | 30 +++++++++++----- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs index 96923e914e..d466071ea2 100644 --- a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -8,11 +8,21 @@ namespace Azure.DataApiBuilder.Config.Converters; /// -/// Converter factory for AzureKeyVaultOptions that does not perform variable replacement. -/// This ensures we can read the raw AKV configuration needed to set up variable replacement. +/// Converter factory for AzureKeyVaultOptions that can optionally perform variable replacement. /// internal class AzureKeyVaultOptionsConverterFactory : JsonConverterFactory { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private readonly bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + internal AzureKeyVaultOptionsConverterFactory(bool replaceEnvVar = false) + { + _replaceEnvVar = replaceEnvVar; + } + /// public override bool CanConvert(Type typeToConvert) { @@ -22,14 +32,24 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AzureKeyVaultOptionsConverter(); + return new AzureKeyVaultOptionsConverter(_replaceEnvVar); } private class AzureKeyVaultOptionsConverter : JsonConverter { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private readonly bool _replaceEnvVar; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + public AzureKeyVaultOptionsConverter(bool replaceEnvVar) + { + _replaceEnvVar = replaceEnvVar; + } + /// - /// Reads AzureKeyVaultOptions without performing variable replacement. - /// Variable replacement will be handled in subsequent passes. + /// Reads AzureKeyVaultOptions with optional variable replacement. /// public override AzureKeyVaultOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -57,7 +77,7 @@ private class AzureKeyVaultOptionsConverter : JsonConverter(ref reader, options); } diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index b2e5560c24..cac6fa6ebd 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -130,12 +130,14 @@ protected void SignalConfigChanged(string message = "") public abstract string GetPublishedDraftSchemaLink(); /// - /// Extracts AzureKeyVaultOptions from JSON string without performing variable replacement. - /// This is needed to get the AKV configuration for setting up variable replacement. + /// Extracts AzureKeyVaultOptions from JSON string with environment variable replacement but no AKV replacement. + /// This is needed to get the actual AKV configuration (resolving any @env() variables) for setting up variable replacement. /// /// JSON that represents the config file. + /// Failure mode for environment variable replacement. /// AzureKeyVaultOptions if present, null otherwise. - private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions(string json) + private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions(string json, + Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode) { JsonSerializerOptions options = new() { @@ -144,8 +146,16 @@ protected void SignalConfigChanged(string message = "") ReadCommentHandling = JsonCommentHandling.Skip }; options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); - options.Converters.Add(new AzureKeyVaultOptionsConverterFactory()); - options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar: false)); + // Enable environment variable replacement for AKV extraction so @env('AKV_ENDPOINT') works + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replaceEnvVar: true)); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar: true)); + + // Add environment variable replacement only (no AKV replacement in first pass) + DeserializationVariableReplacementSettings envOnlySettings = new( + doReplaceEnvVar: true, + doReplaceAKVVar: false, + envFailureMode: replacementFailureMode); + options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); try { @@ -182,14 +192,16 @@ public static bool TryParseConfig(string json, bool replaceEnvVar = false, Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) { - // First pass: extract AzureKeyVault options without variable replacement + // First pass: extract AzureKeyVault options with environment variable replacement (but no AKV replacement) + // This ensures that @env('AKV_ENDPOINT') in the azure-key-vault section gets resolved properly AzureKeyVaultOptions? azureKeyVaultOptions = null; if (replaceEnvVar) { - azureKeyVaultOptions = ExtractAzureKeyVaultOptions(json); + azureKeyVaultOptions = ExtractAzureKeyVaultOptions(json, replacementFailureMode); } - // Create replacement settings based on extracted AKV options + // Second pass: Create replacement settings based on extracted AKV options for full config deserialization + // This enables both environment variable and AKV variable replacement for the entire configuration DeserializationVariableReplacementSettings? replacementSettings = null; if (replaceEnvVar) { @@ -348,7 +360,7 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new FileSinkConverter(replacementSettings?.DoReplaceEnvVar ?? false)); // Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly - options.Converters.Add(new AzureKeyVaultOptionsConverterFactory()); + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); // Only add the extensible string converter if we have replacement settings if (replacementSettings is not null) From bbcc15986366ebbc1a8f93955e1ba39e8a4597a7 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Wed, 17 Sep 2025 14:13:36 -0700 Subject: [PATCH 04/37] fix old bools being used --- src/Cli.Tests/EnvironmentTests.cs | 1 + .../Converters/Utf8JsonReaderExtensions.cs | 1 + ...erializationVariableReplacementSettings.cs | 29 +---- src/Config/FileSystemRuntimeConfigLoader.cs | 3 +- src/Config/RuntimeConfigLoader.cs | 104 ++++++------------ .../Configurations/RuntimeConfigProvider.cs | 6 +- .../Caching/CachingConfigProcessingTests.cs | 40 ++----- .../Configuration/ConfigurationTests.cs | 32 ++++-- ...untimeConfigLoaderJsonDeserializerTests.cs | 12 +- 9 files changed, 83 insertions(+), 145 deletions(-) diff --git a/src/Cli.Tests/EnvironmentTests.cs b/src/Cli.Tests/EnvironmentTests.cs index 02009db283..64680b06d9 100644 --- a/src/Cli.Tests/EnvironmentTests.cs +++ b/src/Cli.Tests/EnvironmentTests.cs @@ -20,6 +20,7 @@ public class EnvironmentTests public void TestInitialize() { DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: false, envFailureMode: EnvironmentVariableReplacementFailureMode.Throw); diff --git a/src/Config/Converters/Utf8JsonReaderExtensions.cs b/src/Config/Converters/Utf8JsonReaderExtensions.cs index acb7c39f5e..0a65cbb282 100644 --- a/src/Config/Converters/Utf8JsonReaderExtensions.cs +++ b/src/Config/Converters/Utf8JsonReaderExtensions.cs @@ -38,6 +38,7 @@ static internal class Utf8JsonReaderExtensions { // Create a simple replacement settings for environment variables only DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: false, envFailureMode: replacementFailureMode); diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index 60c03a51f3..8c548695a0 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -45,38 +45,13 @@ public class DeserializationVariableReplacementSettings public const string INNER_ENV_PATTERN = @"[^@env\(].*(?=\))"; public const string INNER_AKV_PATTERN = @"[^@AKV\(].*(?=\))"; - private AzureKeyVaultOptions? _azureKeyVaultOptions; + private readonly AzureKeyVaultOptions? _azureKeyVaultOptions; private readonly SecretClient? _akvClient; public Dictionary> ReplacementStrategies { get; private set; } = new(); public DeserializationVariableReplacementSettings( - bool doReplaceEnvVar = true, - bool doReplaceAKVVar = true, - EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) - { - DoReplaceEnvVar = doReplaceEnvVar; - DoReplaceAKVVar = doReplaceAKVVar; - EnvFailureMode = envFailureMode; - - if (DoReplaceEnvVar) - { - ReplacementStrategies.Add( - new Regex(INNER_ENV_PATTERN, RegexOptions.Compiled), - ReplaceEnvVariable); - } - - if (DoReplaceAKVVar && _azureKeyVaultOptions is not null) - { - _akvClient = CreateSecretClient(_azureKeyVaultOptions); - ReplacementStrategies.Add( - new Regex(INNER_AKV_PATTERN, RegexOptions.Compiled), - ReplaceAKVVariable); - } - } - - public DeserializationVariableReplacementSettings( - AzureKeyVaultOptions azureKeyVaultOptions, + AzureKeyVaultOptions? azureKeyVaultOptions = null, bool doReplaceEnvVar = true, bool doReplaceAKVVar = true, EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 0cf5828030..c1f256c078 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -226,7 +226,8 @@ public bool TryLoadConfig( } } - if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar)) + if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, + new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true), logger: null, connectionString: _connectionString)) { if (TrySetupConfigFileWatcher()) { diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index cac6fa6ebd..f009c4d17c 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -51,7 +51,7 @@ public RuntimeConfigLoader(HotReloadEventHandler? handler = /// DabChangeToken #pragma warning disable CA1024 // Use properties where appropriate public IChangeToken GetChangeToken() -#pragma warning restore CA1024 // Use properties where appropriate +#pragma warning restore CA1024 // Use properties where Appropriate { return _changeToken; } @@ -130,14 +130,15 @@ protected void SignalConfigChanged(string message = "") public abstract string GetPublishedDraftSchemaLink(); /// - /// Extracts AzureKeyVaultOptions from JSON string with environment variable replacement but no AKV replacement. - /// This is needed to get the actual AKV configuration (resolving any @env() variables) for setting up variable replacement. + /// Extracts AzureKeyVaultOptions from JSON string with configurable variable replacement. /// /// JSON that represents the config file. - /// Failure mode for environment variable replacement. + /// Whether to enable environment variable replacement during extraction. + /// Failure mode for environment variable replacement if enabled. /// AzureKeyVaultOptions if present, null otherwise. private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions(string json, - Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode) + bool enableEnvReplacement, + Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) { JsonSerializerOptions options = new() { @@ -146,16 +147,19 @@ protected void SignalConfigChanged(string message = "") ReadCommentHandling = JsonCommentHandling.Skip }; options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); - // Enable environment variable replacement for AKV extraction so @env('AKV_ENDPOINT') works - options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replaceEnvVar: true)); - options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar: true)); + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replaceEnvVar: enableEnvReplacement)); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar: enableEnvReplacement)); - // Add environment variable replacement only (no AKV replacement in first pass) - DeserializationVariableReplacementSettings envOnlySettings = new( - doReplaceEnvVar: true, - doReplaceAKVVar: false, - envFailureMode: replacementFailureMode); - options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); + // Add environment variable replacement if enabled + if (enableEnvReplacement) + { + DeserializationVariableReplacementSettings envOnlySettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: true, + doReplaceAKVVar: false, + envFailureMode: replacementFailureMode); + options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); + } try { @@ -179,46 +183,32 @@ protected void SignalConfigChanged(string message = "") /// /// JSON that represents the config file. /// The parsed config, or null if it parsed unsuccessfully. - /// True if the config was parsed, otherwise false. + /// Settings for variable replacement during deserialization. If null, no variable replacement will be performed. /// logger to log messages /// connectionString to add to config if specified - /// Whether to replace environment variable with its - /// value or not while deserializing. By default, no replacement happens. - /// Determines failure mode for env variable replacement. + /// True if the config was parsed, otherwise false. public static bool TryParseConfig(string json, [NotNullWhen(true)] out RuntimeConfig? config, + DeserializationVariableReplacementSettings? replacementSettings = null, ILogger? logger = null, - string? connectionString = null, - bool replaceEnvVar = false, - Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) + string? connectionString = null) { - // First pass: extract AzureKeyVault options with environment variable replacement (but no AKV replacement) - // This ensures that @env('AKV_ENDPOINT') in the azure-key-vault section gets resolved properly - AzureKeyVaultOptions? azureKeyVaultOptions = null; - if (replaceEnvVar) - { - azureKeyVaultOptions = ExtractAzureKeyVaultOptions(json, replacementFailureMode); - } - - // Second pass: Create replacement settings based on extracted AKV options for full config deserialization - // This enables both environment variable and AKV variable replacement for the entire configuration - DeserializationVariableReplacementSettings? replacementSettings = null; - if (replaceEnvVar) + // First pass: extract AzureKeyVault options if AKV replacement is requested + if (replacementSettings?.DoReplaceAKVVar == true) { + AzureKeyVaultOptions? azureKeyVaultOptions = ExtractAzureKeyVaultOptions( + json, + enableEnvReplacement: replacementSettings.DoReplaceEnvVar, + replacementFailureMode: replacementSettings.EnvFailureMode); + + // Update replacement settings with the extracted AKV options if (azureKeyVaultOptions is not null) { replacementSettings = new DeserializationVariableReplacementSettings( - azureKeyVaultOptions, - doReplaceEnvVar: true, - doReplaceAKVVar: true, - envFailureMode: replacementFailureMode); - } - else - { - replacementSettings = new DeserializationVariableReplacementSettings( - doReplaceEnvVar: true, - doReplaceAKVVar: false, - envFailureMode: replacementFailureMode); + azureKeyVaultOptions: azureKeyVaultOptions, + doReplaceEnvVar: replacementSettings.DoReplaceEnvVar, + doReplaceAKVVar: replacementSettings.DoReplaceAKVVar, + envFailureMode: replacementSettings.EnvFailureMode); } } @@ -255,11 +245,11 @@ public static bool TryParseConfig(string json, DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey); // Add Application Name for telemetry for MsSQL or PgSql - if (ds.DatabaseType is DatabaseType.MSSQL && replaceEnvVar) + if (ds.DatabaseType is DatabaseType.MSSQL && replacementSettings?.DoReplaceEnvVar == true) { updatedConnection = GetConnectionStringWithApplicationName(connectionValue); } - else if (ds.DatabaseType is DatabaseType.PostgreSQL && replaceEnvVar) + else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true) { updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue); } @@ -297,28 +287,6 @@ ex is JsonException || return true; } - /// - /// Get Serializer options for the config file. - /// - /// Whether to replace environment variable with value or not while deserializing. - /// By default, no replacement happens. - /// Determines failure mode for env variable replacement. - public static JsonSerializerOptions GetSerializationOptions( - bool replaceEnvVar = false, - Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) - { - DeserializationVariableReplacementSettings? replacementSettings = null; - if (replaceEnvVar) - { - replacementSettings = new DeserializationVariableReplacementSettings( - doReplaceEnvVar: true, - doReplaceAKVVar: false, // No AKV replacement without explicit AKV options - envFailureMode: replacementFailureMode); - } - - return GetSerializationOptions(replacementSettings); - } - /// /// Get Serializer options for the config file. /// diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index faeb2b94d0..631a7a951e 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -189,8 +189,7 @@ public async Task Initialize( if (RuntimeConfigLoader.TryParseConfig( configuration, out RuntimeConfig? runtimeConfig, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Ignore)) + replacementSettings: null)) { _configLoader.RuntimeConfig = runtimeConfig; @@ -272,7 +271,8 @@ public async Task Initialize( IsLateConfigured = true; - if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, replaceEnvVar: replaceEnvVar, replacementFailureMode: replacementFailureMode)) + if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, + new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true, envFailureMode: replacementFailureMode))) { _configLoader.RuntimeConfig = runtimeConfig.DataSource.DatabaseType switch { diff --git a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs index 2780af63c5..d42388621b 100644 --- a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs +++ b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs @@ -56,10 +56,7 @@ public void EntityCacheOptionsDeserialization_ValidJson( RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -103,10 +100,7 @@ public void EntityCacheOptionsDeserialization_InvalidValues(string entityCacheCo bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( json: fullConfig, out _, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsFalse(isParsingSuccessful, message: "Expected JSON parsing to fail."); @@ -141,10 +135,7 @@ public void GlobalCacheOptionsDeserialization_ValidValues( RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -187,10 +178,7 @@ public void GlobalCacheOptionsDeserialization_InvalidValues(string globalCacheCo bool parsingSuccessful = RuntimeConfigLoader.TryParseConfig( json: fullConfig, out _, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsFalse(parsingSuccessful, message: "Expected JSON parsing to fail."); @@ -216,10 +204,7 @@ public void GlobalCacheOptionsOverridesEntityCacheOptions(string globalCacheConf RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -252,10 +237,7 @@ public void UserDefinedTtlWrittenToSerializedJsonConfigFile(bool expectIsUserDef RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act @@ -300,10 +282,7 @@ public void CachePropertyNotWrittenToSerializedJsonConfigFile(string cacheConfig RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act @@ -342,10 +321,7 @@ public void DefaultTtlNotWrittenToSerializedJsonConfigFile(string cacheConfig) RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 2522806049..c0d24bd9d1 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -840,7 +840,7 @@ public void MsSqlConnStringSupplementedWithAppNameProperty( bool configParsed = RuntimeConfigLoader.TryParseConfig( runtimeConfig.ToJson(), out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + new ()); // Assert Assert.AreEqual( @@ -893,7 +893,7 @@ public void PgSqlConnStringSupplementedWithAppNameProperty( bool configParsed = RuntimeConfigLoader.TryParseConfig( runtimeConfig.ToJson(), out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + new ()); // Assert Assert.AreEqual( @@ -958,7 +958,7 @@ public void TestConnectionStringIsCorrectlyUpdatedWithApplicationName( bool configParsed = RuntimeConfigLoader.TryParseConfig( runtimeConfig.ToJson(), out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + new ()); // Assert Assert.AreEqual( @@ -2342,7 +2342,12 @@ public async Task TestSPRestDefaultsForManuallyConstructedConfigs( HttpStatusCode expectedResponseStatusCode) { string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, entityJson); - RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); + RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); string configFileName = "custom-config.json"; File.WriteAllText(configFileName, deserializedConfig.ToJson()); string[] args = new[] @@ -2425,7 +2430,12 @@ public async Task SanityTestForRestAndGQLRequestsWithoutMultipleMutationFeatureF // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the // configuration file (instead of using CLI). string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, BOOK_ENTITY_JSON); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); string configFileName = "custom-config.json"; File.WriteAllText(configFileName, deserializedConfig.ToJson()); string[] args = new[] @@ -3278,7 +3288,12 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr // The BASE_CONFIG omits the rest.request-body-strict option in the runtime section. string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, entityJson); - RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); + RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, deserializedConfig.ToJson()); string[] args = new[] @@ -3292,7 +3307,8 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(SupportedHttpVerb.Post); string requestBody = @"{ ""title"": ""Harry Potter and the Order of Phoenix"", - ""publisher_id"": 1234"; + ""publisher_id"": 1234 + }"; if (includeExtraneousFieldInRequestBody) { @@ -5459,7 +5475,7 @@ public static string GetConnectionStringFromEnvironmentConfig(string environment string sqlFile = new FileSystemRuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true); string configPayload = File.ReadAllText(sqlFile); - RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig, replaceEnvVar: true); + RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig, replacementSettings: new()); return runtimeConfig.DataSource.ConnectionString; } diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 4be71adc5e..e200bcc3d9 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -78,18 +78,18 @@ public void CheckConfigEnvParsingTest( if (replaceEnvVar) { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), "Should read the expected config"); } else { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), "Should read the expected config"); } Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), "Should read actual config"); Assert.AreEqual(expectedConfig.ToJson(), actualConfig.ToJson()); } @@ -129,7 +129,7 @@ public void TestConfigParsingWithEnvVarReplacement(bool replaceEnvVar, string da string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceConfigForGivenDatabase(databaseType)); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replaceEnvVar: replaceEnvVar); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -177,7 +177,7 @@ public void TestConfigParsingWhenDataSourceOptionsForCosmosDBContainsInvalidValu string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceOptionsForCosmosDBWithInvalidValues()); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replaceEnvVar: true); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -320,7 +320,7 @@ public void TestDataSourceDeserializationFailures(string dbType, string connecti ""entities"":{ } }"; // replaceEnvVar: true is needed to make sure we do post-processing for the connection string case - Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replaceEnvVar: true)); + Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true))); Assert.IsNull(deserializedConfig); } From f6f92bd802d3359aab803eacf80e52d2eb949069 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Wed, 17 Sep 2025 18:01:46 -0700 Subject: [PATCH 05/37] more bool replacements --- .../AKVRetryPolicyOptionsConverterFactory.cs | 30 ++++++------- .../AzureKeyVaultOptionsConverterFactory.cs | 16 +++---- .../AzureLogAnalyticsAuthOptionsConverter.cs | 19 ++++---- ...zureLogAnalyticsOptionsConverterFactory.cs | 32 +++++++------ .../Converters/DataSourceConverterFactory.cs | 34 +++++++------- ...DatasourceHealthOptionsConvertorFactory.cs | 16 +++---- .../EntityCacheOptionsConverterFactory.cs | 16 +++---- .../EntityGraphQLOptionsConverterFactory.cs | 38 ++++++++-------- .../EntityRestOptionsConverterFactory.cs | 34 +++++++------- .../EntitySourceConverterFactory.cs | 30 ++++++------- src/Config/Converters/FileSinkConverter.cs | 21 +++++---- .../GraphQLRuntimeOptionsConverterFactory.cs | 28 ++++++------ .../RuntimeHealthOptionsConvertorFactory.cs | 16 +++---- .../Converters/Utf8JsonReaderExtensions.cs | 11 +---- src/Config/RuntimeConfigLoader.cs | 45 ++++++++++--------- .../Caching/CachingConfigProcessingTests.cs | 1 - 16 files changed, 183 insertions(+), 204 deletions(-) diff --git a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs index 06d00b64d3..083d518ce3 100644 --- a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs +++ b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs @@ -12,9 +12,8 @@ namespace Azure.DataApiBuilder.Config.Converters; /// internal class AKVRetryPolicyOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +24,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AKVRetryPolicyOptionsConverter(_replaceEnvVar); + return new AKVRetryPolicyOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AKVRetryPolicyOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AKVRetryPolicyOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class AKVRetryPolicyOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AKVRetryPolicyOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AKVRetryPolicyOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -82,7 +80,7 @@ public AKVRetryPolicyOptionsConverter(bool replaceEnvVar) } else { - mode = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + mode = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); } break; diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs index d466071ea2..fe1e762331 100644 --- a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -14,13 +14,13 @@ internal class AzureKeyVaultOptionsConverterFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private readonly bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - internal AzureKeyVaultOptionsConverterFactory(bool replaceEnvVar = false) + internal AzureKeyVaultOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -32,20 +32,20 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AzureKeyVaultOptionsConverter(_replaceEnvVar); + return new AzureKeyVaultOptionsConverter(_replacementSettings); } private class AzureKeyVaultOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private readonly bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - public AzureKeyVaultOptionsConverter(bool replaceEnvVar) + public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -77,7 +77,7 @@ public AzureKeyVaultOptionsConverter(bool replaceEnvVar) case "endpoint": if (reader.TokenType is JsonTokenType.String) { - endpoint = reader.DeserializeString(_replaceEnvVar); + endpoint = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs index 1428c0d75f..d4b7623aa2 100644 --- a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs +++ b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs @@ -9,15 +9,14 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class AzureLogAnalyticsAuthOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AzureLogAnalyticsAuthOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -48,7 +47,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "custom-table-name": if (reader.TokenType is not JsonTokenType.Null) { - customTableName = reader.DeserializeString(_replaceEnvVar); + customTableName = reader.DeserializeString(_replacementSettings); } break; @@ -56,7 +55,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "dcr-immutable-id": if (reader.TokenType is not JsonTokenType.Null) { - dcrImmutableId = reader.DeserializeString(_replaceEnvVar); + dcrImmutableId = reader.DeserializeString(_replacementSettings); } break; @@ -64,7 +63,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "dce-endpoint": if (reader.TokenType is not JsonTokenType.Null) { - dceEndpoint = reader.DeserializeString(_replaceEnvVar); + dceEndpoint = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs index 3fcbe8c7bd..fc7c72d655 100644 --- a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs @@ -12,9 +12,8 @@ namespace Azure.DataApiBuilder.Config.Converters; /// internal class AzureLogAnalyticsOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +24,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AzureLogAnalyticsOptionsConverter(_replaceEnvVar); + return new AzureLogAnalyticsOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AzureLogAnalyticsOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AzureLogAnalyticsOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class AzureLogAnalyticsOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AzureLogAnalyticsOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -57,7 +55,7 @@ internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) { if (reader.TokenType is JsonTokenType.StartObject) { - AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = new(_replaceEnvVar); + AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = new(_replacementSettings); bool? enabled = null; AzureLogAnalyticsAuthOptions? auth = null; @@ -91,7 +89,7 @@ internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) case "dab-identifier": if (reader.TokenType is not JsonTokenType.Null) { - logType = reader.DeserializeString(_replaceEnvVar); + logType = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/DataSourceConverterFactory.cs b/src/Config/Converters/DataSourceConverterFactory.cs index dabbee405e..1788ebf2b4 100644 --- a/src/Config/Converters/DataSourceConverterFactory.cs +++ b/src/Config/Converters/DataSourceConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class DataSourceConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new DataSourceConverter(_replaceEnvVar); + return new DataSourceConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal DataSourceConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal DataSourceConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class DataSourceConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public DataSourceConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public DataSourceConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override DataSource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -69,11 +67,11 @@ public DataSourceConverter(bool replaceEnvVar) switch (propertyName) { case "database-type": - databaseType = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + databaseType = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); break; case "connection-string": - connectionString = reader.DeserializeString(replaceEnvVar: _replaceEnvVar)!; + connectionString = reader.DeserializeString(_replacementSettings)!; break; case "health": @@ -106,7 +104,7 @@ public DataSourceConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { // Determine whether to resolve the environment variable or keep as-is. - string stringValue = reader.DeserializeString(replaceEnvVar: _replaceEnvVar)!; + string stringValue = reader.DeserializeString(_replacementSettings)!; if (bool.TryParse(stringValue, out bool boolValue)) { diff --git a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs index 52272c57a7..af1fd381aa 100644 --- a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class DataSourceHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +22,27 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new HealthCheckOptionsConverter(_replaceEnvVar); + return new HealthCheckOptionsConverter(_replacementSettings); } /// Whether to replace environment variable with its /// value or not while deserializing. - internal DataSourceHealthOptionsConvertorFactory(bool replaceEnvVar) + internal DataSourceHealthOptionsConvertorFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class HealthCheckOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - public HealthCheckOptionsConverter(bool replaceEnvVar) + public HealthCheckOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -85,7 +85,7 @@ public HealthCheckOptionsConverter(bool replaceEnvVar) case "name": if (reader.TokenType is not JsonTokenType.Null) { - name = reader.DeserializeString(_replaceEnvVar); + name = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs index 32a616ab81..7211780893 100644 --- a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs @@ -14,7 +14,7 @@ internal class EntityCacheOptionsConverterFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +25,27 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityCacheOptionsConverter(_replaceEnvVar); + return new EntityCacheOptionsConverter(_replacementSettings); } /// Whether to replace environment variable with its /// value or not while deserializing. - internal EntityCacheOptionsConverterFactory(bool replaceEnvVar) + internal EntityCacheOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntityCacheOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - public EntityCacheOptionsConverter(bool replaceEnvVar) + public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -110,7 +110,7 @@ public EntityCacheOptionsConverter(bool replaceEnvVar) throw new JsonException("level property cannot be null."); } - level = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + level = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); break; } diff --git a/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs index 576850b1cb..abe094e970 100644 --- a/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntityGraphQLOptionsConverterFactory : JsonConverterFactory { - /// Determines whether to replace environment variable with its - /// value or not while deserializing. - private bool _replaceEnvVar; + /// Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityGraphQLOptionsConverter(_replaceEnvVar); + return new EntityGraphQLOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntityGraphQLOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntityGraphQLOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntityGraphQLOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntityGraphQLOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntityGraphQLOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -73,7 +71,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) case "type": if (reader.TokenType is JsonTokenType.String) { - singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + singular = reader.DeserializeString(_replacementSettings) ?? string.Empty; } else if (reader.TokenType is JsonTokenType.StartObject) { @@ -95,10 +93,10 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) switch (property2) { case "singular": - singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + singular = reader.DeserializeString(_replacementSettings) ?? string.Empty; break; case "plural": - plural = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + plural = reader.DeserializeString(_replacementSettings) ?? string.Empty; break; } } @@ -112,7 +110,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) break; case "operation": - string? op = reader.DeserializeString(_replaceEnvVar); + string? op = reader.DeserializeString(_replacementSettings); if (op is not null) { @@ -136,7 +134,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { - string? singular = reader.DeserializeString(_replaceEnvVar); + string? singular = reader.DeserializeString(_replacementSettings); return new EntityGraphQLOptions(singular ?? string.Empty, string.Empty); } diff --git a/src/Config/Converters/EntityRestOptionsConverterFactory.cs b/src/Config/Converters/EntityRestOptionsConverterFactory.cs index cc33943caa..f8c9096673 100644 --- a/src/Config/Converters/EntityRestOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityRestOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntityRestOptionsConverterFactory : JsonConverterFactory { - /// Determines whether to replace environment variable with its - /// value or not while deserializing. - private bool _replaceEnvVar; + /// Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityRestOptionsConverter(_replaceEnvVar); + return new EntityRestOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntityRestOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntityRestOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } internal class EntityRestOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntityRestOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntityRestOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -67,7 +65,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String || reader.TokenType is JsonTokenType.Null) { - restOptions = restOptions with { Path = reader.DeserializeString(_replaceEnvVar) }; + restOptions = restOptions with { Path = reader.DeserializeString(_replacementSettings) }; break; } @@ -87,7 +85,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) break; } - methods.Add(EnumExtensions.Deserialize(reader.DeserializeString(replaceEnvVar: true)!)); + methods.Add(EnumExtensions.Deserialize(reader.DeserializeString(new DeserializationVariableReplacementSettings())!)); } restOptions = restOptions with { Methods = methods.ToArray() }; @@ -107,7 +105,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { - return new EntityRestOptions(Array.Empty(), reader.DeserializeString(_replaceEnvVar), true); + return new EntityRestOptions(Array.Empty(), reader.DeserializeString(_replacementSettings), true); } if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) diff --git a/src/Config/Converters/EntitySourceConverterFactory.cs b/src/Config/Converters/EntitySourceConverterFactory.cs index 51af00717d..b8b9384130 100644 --- a/src/Config/Converters/EntitySourceConverterFactory.cs +++ b/src/Config/Converters/EntitySourceConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntitySourceConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,34 +21,33 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntitySourceConverter(_replaceEnvVar); + return new EntitySourceConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntitySourceConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntitySourceConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntitySourceConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntitySourceConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntitySourceConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override EntitySource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { - string? obj = reader.DeserializeString(_replaceEnvVar); + string? obj = reader.DeserializeString(_replacementSettings); return new EntitySource(obj ?? string.Empty, EntitySourceType.Table, new(), Array.Empty()); } diff --git a/src/Config/Converters/FileSinkConverter.cs b/src/Config/Converters/FileSinkConverter.cs index cc7d138a1b..4299fb913b 100644 --- a/src/Config/Converters/FileSinkConverter.cs +++ b/src/Config/Converters/FileSinkConverter.cs @@ -7,18 +7,17 @@ using Serilog; namespace Azure.DataApiBuilder.Config.Converters; + class FileSinkConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; - - /// - /// Whether to replace environment variable with its value or not while deserializing. - /// - public FileSinkConverter(bool replaceEnvVar) + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public FileSinkConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -59,7 +58,7 @@ public FileSinkConverter(bool replaceEnvVar) case "path": if (reader.TokenType is not JsonTokenType.Null) { - path = reader.DeserializeString(_replaceEnvVar); + path = reader.DeserializeString(_replacementSettings); } break; @@ -67,7 +66,7 @@ public FileSinkConverter(bool replaceEnvVar) case "rolling-interval": if (reader.TokenType is not JsonTokenType.Null) { - rollingInterval = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + rollingInterval = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); } break; diff --git a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs index 082c982e7e..109caef0d5 100644 --- a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class GraphQLRuntimeOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,25 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new GraphQLRuntimeOptionsConverter(_replaceEnvVar); + return new GraphQLRuntimeOptionsConverter(_replacementSettings); } - internal GraphQLRuntimeOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal GraphQLRuntimeOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class GraphQLRuntimeOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal GraphQLRuntimeOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override GraphQLRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -117,7 +117,7 @@ internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) case "path": if (reader.TokenType is JsonTokenType.String) { - string? path = reader.DeserializeString(_replaceEnvVar); + string? path = reader.DeserializeString(_replacementSettings); if (path is null) { path = "/graphql"; diff --git a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs index d49cc264e7..2cfcd720e9 100644 --- a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class RuntimeHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,25 +22,25 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new HealthCheckOptionsConverter(_replaceEnvVar); + return new HealthCheckOptionsConverter(_replacementSettings); } - internal RuntimeHealthOptionsConvertorFactory(bool replaceEnvVar) + internal RuntimeHealthOptionsConvertorFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class HealthCheckOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - internal HealthCheckOptionsConverter(bool replaceEnvVar) + internal HealthCheckOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -102,7 +102,7 @@ internal HealthCheckOptionsConverter(bool replaceEnvVar) { if (reader.TokenType == JsonTokenType.String) { - string? currentRole = reader.DeserializeString(_replaceEnvVar); + string? currentRole = reader.DeserializeString(_replacementSettings); if (!string.IsNullOrEmpty(currentRole)) { stringList.Add(currentRole); diff --git a/src/Config/Converters/Utf8JsonReaderExtensions.cs b/src/Config/Converters/Utf8JsonReaderExtensions.cs index 0a65cbb282..66c83de52d 100644 --- a/src/Config/Converters/Utf8JsonReaderExtensions.cs +++ b/src/Config/Converters/Utf8JsonReaderExtensions.cs @@ -19,8 +19,7 @@ static internal class Utf8JsonReaderExtensions /// The result of deserialization. /// Thrown if the is not String. public static string? DeserializeString(this Utf8JsonReader reader, - bool replaceEnvVar, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings) { if (reader.TokenType is JsonTokenType.Null) { @@ -34,14 +33,8 @@ static internal class Utf8JsonReaderExtensions // Add the StringConverterFactory so that we can do environment variable substitution. JsonSerializerOptions options = new(); - if (replaceEnvVar) + if (replacementSettings is not null) { - // Create a simple replacement settings for environment variables only - DeserializationVariableReplacementSettings replacementSettings = new( - azureKeyVaultOptions: null, - doReplaceEnvVar: true, - doReplaceAKVVar: false, - envFailureMode: replacementFailureMode); options.Converters.Add(new StringJsonConverterFactory(replacementSettings)); } diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index f009c4d17c..1133cd2288 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -146,19 +146,20 @@ protected void SignalConfigChanged(string message = "") PropertyNamingPolicy = new HyphenatedNamingPolicy(), ReadCommentHandling = JsonCommentHandling.Skip }; + DeserializationVariableReplacementSettings envOnlySettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: enableEnvReplacement, + doReplaceAKVVar: false, + envFailureMode: replacementFailureMode); + options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); - options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replaceEnvVar: enableEnvReplacement)); - options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar: enableEnvReplacement)); - + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings: envOnlySettings)); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings: envOnlySettings)); + // Add environment variable replacement if enabled if (enableEnvReplacement) { - DeserializationVariableReplacementSettings envOnlySettings = new( - azureKeyVaultOptions: null, - doReplaceEnvVar: true, - doReplaceAKVVar: false, - envFailureMode: replacementFailureMode); - options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); + } try @@ -305,30 +306,30 @@ public static JsonSerializerOptions GetSerializationOptions( Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); - options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replacementSettings?.DoReplaceEnvVar ?? false)); - options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replacementSettings)); + options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replacementSettings)); options.Converters.Add(new EntityHealthOptionsConvertorFactory()); options.Converters.Add(new RestRuntimeOptionsConverterFactory()); - options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); - options.Converters.Add(new EntitySourceConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); - options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); - options.Converters.Add(new EntityRestOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new EntitySourceConverterFactory(replacementSettings)); + options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new EntityRestOptionsConverterFactory(replacementSettings)); options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); - options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); options.Converters.Add(new MultipleCreateOptionsConverter()); options.Converters.Add(new MultipleMutationOptionsConverter(options)); - options.Converters.Add(new DataSourceConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new DataSourceConverterFactory(replacementSettings)); options.Converters.Add(new HostOptionsConvertorFactory()); - options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); - options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); - options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings?.DoReplaceEnvVar ?? false)); - options.Converters.Add(new FileSinkConverter(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings)); + options.Converters.Add(new FileSinkConverter(replacementSettings)); // Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly - options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings?.DoReplaceEnvVar ?? false)); + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings)); // Only add the extensible string converter if we have replacement settings if (replacementSettings is not null) diff --git a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs index d42388621b..a6daebf3e4 100644 --- a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs +++ b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs @@ -5,7 +5,6 @@ using System.Text; using System.Text.Json; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; From 0dd7ad62aabf530b30a336d7d00fc70fb4116da5 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Wed, 17 Sep 2025 18:12:40 -0700 Subject: [PATCH 06/37] refactor out bools --- src/Core/Configurations/RuntimeConfigProvider.cs | 7 ++----- src/Service/Controllers/ConfigurationController.cs | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index 631a7a951e..b46a716f48 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -6,7 +6,6 @@ using System.IO.Abstractions; using System.Net; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.NamingPolicies; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; @@ -256,8 +255,7 @@ public async Task Initialize( string? graphQLSchema, string connectionString, string? accessToken, - bool replaceEnvVar = true, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings) { if (string.IsNullOrEmpty(connectionString)) { @@ -271,8 +269,7 @@ public async Task Initialize( IsLateConfigured = true; - if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, - new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true, envFailureMode: replacementFailureMode))) + if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, replacementSettings)) { _configLoader.RuntimeConfig = runtimeConfig.DataSource.DatabaseType switch { diff --git a/src/Service/Controllers/ConfigurationController.cs b/src/Service/Controllers/ConfigurationController.cs index be3f9bd727..a15a2f7577 100644 --- a/src/Service/Controllers/ConfigurationController.cs +++ b/src/Service/Controllers/ConfigurationController.cs @@ -91,8 +91,8 @@ public async Task Index([FromBody] ConfigurationPostParameters con configuration.Schema, configuration.ConnectionString, configuration.AccessToken, - replaceEnvVar: false, - replacementFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore); + replacementSettings: new(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAKVVar: false, envFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore) + ); if (initResult && _configurationProvider.TryGetConfig(out _)) { From ac4356eabb94c294a3edc4a65c3369d8190a6663 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Tue, 21 Oct 2025 14:36:05 -0700 Subject: [PATCH 07/37] addressing comments --- .../AKVRetryPolicyOptionsConverterFactory.cs | 2 ++ .../AzureKeyVaultOptionsConverterFactory.cs | 12 ++++++------ .../Converters/EntityCacheOptionsConverterFactory.cs | 6 ++---- src/Config/Converters/StringJsonConverterFactory.cs | 2 +- src/Config/Converters/Utf8JsonReaderExtensions.cs | 3 +-- .../DeserializationVariableReplacementSettings.cs | 12 ++++++------ 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs index 083d518ce3..8ecd86562f 100644 --- a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs +++ b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs @@ -13,6 +13,7 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class AKVRetryPolicyOptionsConverterFactory : JsonConverterFactory { // Settings for variable replacement during deserialization. + // Currently allows for Azure Key Vault and Environment Variable replacement. private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// @@ -37,6 +38,7 @@ internal AKVRetryPolicyOptionsConverterFactory(DeserializationVariableReplacemen private class AKVRetryPolicyOptionsConverter : JsonConverter { // Settings for variable replacement during deserialization. + // Currently allows for Azure Key Vault and Environment Variable replacement. private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// Settings for variable replacement during deserialization. diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs index fe1e762331..b6e70f05b6 100644 --- a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -53,6 +53,11 @@ public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? /// public override AzureKeyVaultOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType is JsonTokenType.Null) + { + return null; + } + if (reader.TokenType is JsonTokenType.StartObject) { string? endpoint = null; @@ -92,15 +97,10 @@ public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? break; default: - reader.Skip(); - break; + throw new JsonException($"Unexpected property {property}"); } } } - else if (reader.TokenType is JsonTokenType.Null) - { - return null; - } throw new JsonException("Invalid AzureKeyVaultOptions format"); } diff --git a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs index 7211780893..175cdae937 100644 --- a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs @@ -28,8 +28,7 @@ public override bool CanConvert(Type typeToConvert) return new EntityCacheOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. + /// The replacement settings to use while deserializing. internal EntityCacheOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings) { _replacementSettings = replacementSettings; @@ -41,8 +40,7 @@ private class EntityCacheOptionsConverter : JsonConverter // value or not while deserializing. private DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. + /// The replacement settings to use while deserializing. public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { _replacementSettings = replacementSettings; diff --git a/src/Config/Converters/StringJsonConverterFactory.cs b/src/Config/Converters/StringJsonConverterFactory.cs index 2e581ce55e..102e01d628 100644 --- a/src/Config/Converters/StringJsonConverterFactory.cs +++ b/src/Config/Converters/StringJsonConverterFactory.cs @@ -44,7 +44,7 @@ public StringJsonConverter(DeserializationVariableReplacementSettings replacemen if (reader.TokenType == JsonTokenType.String) { string? value = reader.GetString(); - if (string.IsNullOrEmpty(value)) + if (string.IsNullOrWhiteSpace(value)) { return value; } diff --git a/src/Config/Converters/Utf8JsonReaderExtensions.cs b/src/Config/Converters/Utf8JsonReaderExtensions.cs index 66c83de52d..5e16357227 100644 --- a/src/Config/Converters/Utf8JsonReaderExtensions.cs +++ b/src/Config/Converters/Utf8JsonReaderExtensions.cs @@ -13,8 +13,7 @@ static internal class Utf8JsonReaderExtensions /// substitution is applied. /// /// The reader that we want to pull the string from. - /// Whether to replace environment variable with its - /// value or not while deserializing. + /// The replacement settings to use while deserializing. /// The failure mode to use when replacing environment variables. /// The result of deserialization. /// Thrown if the is not String. diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index 8c548695a0..b5e4a30d2e 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -14,7 +14,7 @@ namespace Azure.DataApiBuilder.Config public class DeserializationVariableReplacementSettings { public bool DoReplaceEnvVar { get; set; } = true; - public bool DoReplaceAKVVar { get; set; } = true; + public bool DoReplaceAkvVar { get; set; } = true; public EnvironmentVariableReplacementFailureMode EnvFailureMode { get; set; } = EnvironmentVariableReplacementFailureMode.Throw; // @env\(' : match @env(' @@ -53,12 +53,12 @@ public class DeserializationVariableReplacementSettings public DeserializationVariableReplacementSettings( AzureKeyVaultOptions? azureKeyVaultOptions = null, bool doReplaceEnvVar = true, - bool doReplaceAKVVar = true, + bool doReplaceAkvVar = true, EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) { _azureKeyVaultOptions = azureKeyVaultOptions; DoReplaceEnvVar = doReplaceEnvVar; - DoReplaceAKVVar = doReplaceAKVVar; + DoReplaceAkvVar = doReplaceAkvVar; EnvFailureMode = envFailureMode; if (DoReplaceEnvVar) @@ -68,12 +68,12 @@ public DeserializationVariableReplacementSettings( ReplaceEnvVariable); } - if (DoReplaceAKVVar && _azureKeyVaultOptions is not null) + if (DoReplaceAkvVar && _azureKeyVaultOptions is not null) { _akvClient = CreateSecretClient(_azureKeyVaultOptions); ReplacementStrategies.Add( new Regex(INNER_AKV_PATTERN, RegexOptions.Compiled), - ReplaceAKVVariable); + ReplaceAkvVariable); } } @@ -96,7 +96,7 @@ private string ReplaceEnvVariable(Match match) } } - private string ReplaceAKVVariable(Match match) + private string ReplaceAkvVariable(Match match) { // strips first and last characters, ie: '''hello'' --> ''hello' string name = Regex.Match(match.Value, INNER_AKV_PATTERN).Value[1..^1]; From 3254af5f0c1a372e7c711964f2faf7be11798ead Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 30 Oct 2025 11:06:55 -0700 Subject: [PATCH 08/37] update names --- src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs | 2 +- src/Config/DeserializationVariableReplacementSettings.cs | 4 ++-- src/Config/FileSystemRuntimeConfigLoader.cs | 2 +- src/Config/RuntimeConfigLoader.cs | 6 +++--- src/Service/Controllers/ConfigurationController.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs index 1d6dd9f7c4..60feb7840a 100644 --- a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs +++ b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs @@ -114,7 +114,7 @@ public JsonStringEnumConverterEx() public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Always replace env variable in case of Enum otherwise string to enum conversion will fail. - string? stringValue = reader.DeserializeString(replaceEnvVar: true); + string? stringValue = reader.DeserializeString(new()); if (stringValue == null) { diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index b5e4a30d2e..198b9a1a65 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -64,7 +64,7 @@ public DeserializationVariableReplacementSettings( if (DoReplaceEnvVar) { ReplacementStrategies.Add( - new Regex(INNER_ENV_PATTERN, RegexOptions.Compiled), + new Regex(OUTER_ENV_PATTERN, RegexOptions.Compiled), ReplaceEnvVariable); } @@ -72,7 +72,7 @@ public DeserializationVariableReplacementSettings( { _akvClient = CreateSecretClient(_azureKeyVaultOptions); ReplacementStrategies.Add( - new Regex(INNER_AKV_PATTERN, RegexOptions.Compiled), + new Regex(OUTER_AKV_PATTERN, RegexOptions.Compiled), ReplaceAkvVariable); } } diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index c1f256c078..184b53aa49 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -227,7 +227,7 @@ public bool TryLoadConfig( } if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, - new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true), logger: null, connectionString: _connectionString)) + new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true), logger: null, connectionString: _connectionString)) { if (TrySetupConfigFileWatcher()) { diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 1133cd2288..11c1935cf1 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -149,7 +149,7 @@ protected void SignalConfigChanged(string message = "") DeserializationVariableReplacementSettings envOnlySettings = new( azureKeyVaultOptions: null, doReplaceEnvVar: enableEnvReplacement, - doReplaceAKVVar: false, + doReplaceAkvVar: false, envFailureMode: replacementFailureMode); options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); @@ -195,7 +195,7 @@ public static bool TryParseConfig(string json, string? connectionString = null) { // First pass: extract AzureKeyVault options if AKV replacement is requested - if (replacementSettings?.DoReplaceAKVVar == true) + if (replacementSettings?.DoReplaceAkvVar == true) { AzureKeyVaultOptions? azureKeyVaultOptions = ExtractAzureKeyVaultOptions( json, @@ -208,7 +208,7 @@ public static bool TryParseConfig(string json, replacementSettings = new DeserializationVariableReplacementSettings( azureKeyVaultOptions: azureKeyVaultOptions, doReplaceEnvVar: replacementSettings.DoReplaceEnvVar, - doReplaceAKVVar: replacementSettings.DoReplaceAKVVar, + doReplaceAkvVar: replacementSettings.DoReplaceAkvVar, envFailureMode: replacementSettings.EnvFailureMode); } } diff --git a/src/Service/Controllers/ConfigurationController.cs b/src/Service/Controllers/ConfigurationController.cs index a15a2f7577..4ad8fb40f4 100644 --- a/src/Service/Controllers/ConfigurationController.cs +++ b/src/Service/Controllers/ConfigurationController.cs @@ -91,7 +91,7 @@ public async Task Index([FromBody] ConfigurationPostParameters con configuration.Schema, configuration.ConnectionString, configuration.AccessToken, - replacementSettings: new(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAKVVar: false, envFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore) + replacementSettings: new(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAkvVar: false, envFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore) ); if (initResult && _configurationProvider.TryGetConfig(out _)) From 5e79339015517d7ae83c6253f20cae8256b778e9 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 30 Oct 2025 13:28:41 -0700 Subject: [PATCH 09/37] cleanup --- src/Cli.Tests/EnvironmentTests.cs | 2 +- .../Converters/McpRuntimeOptionsConverterFactory.cs | 8 ++++---- .../UnitTests/MySqlQueryExecutorUnitTests.cs | 3 ++- .../UnitTests/PostgreSqlQueryExecutorUnitTests.cs | 3 ++- .../RuntimeConfigLoaderJsonDeserializerTests.cs | 12 ++++++------ .../UnitTests/SqlQueryExecutorUnitTests.cs | 3 ++- 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Cli.Tests/EnvironmentTests.cs b/src/Cli.Tests/EnvironmentTests.cs index 64680b06d9..08653aaa41 100644 --- a/src/Cli.Tests/EnvironmentTests.cs +++ b/src/Cli.Tests/EnvironmentTests.cs @@ -22,7 +22,7 @@ public void TestInitialize() DeserializationVariableReplacementSettings replacementSettings = new( azureKeyVaultOptions: null, doReplaceEnvVar: true, - doReplaceAKVVar: false, + doReplaceAkvVar: false, envFailureMode: EnvironmentVariableReplacementFailureMode.Throw); StringJsonConverterFactory converterFactory = new(replacementSettings); diff --git a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs index d04182a3db..4143cb395a 100644 --- a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs @@ -14,7 +14,7 @@ internal class McpRuntimeOptionsConverterFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private DeserializationVariableReplacementSettings _replacementSettings; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -28,7 +28,7 @@ public override bool CanConvert(Type typeToConvert) return new McpRuntimeOptionsConverter(_replacementSettings); } - internal McpRuntimeOptionsConverterFactory(DeserializationVariableReplacementSettings replacementSettings) + internal McpRuntimeOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings) { _replacementSettings = replacementSettings; } @@ -37,11 +37,11 @@ private class McpRuntimeOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private DeserializationVariableReplacementSettings _replacementSettings; + private DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - internal McpRuntimeOptionsConverter(DeserializationVariableReplacementSettings replacementSettings) + internal McpRuntimeOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { _replacementSettings = replacementSettings; } diff --git a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs index cbfef36664..df92090dac 100644 --- a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs @@ -81,7 +81,8 @@ await provider.Initialize( provider.GetConfig().ToJson(), graphQLSchema: null, connectionString: connectionString, - accessToken: CONFIG_TOKEN); + accessToken: CONFIG_TOKEN, + new()); mySqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } diff --git a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs index ccaa90b353..a0b0b34108 100644 --- a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs @@ -89,7 +89,8 @@ await provider.Initialize( provider.GetConfig().ToJson(), graphQLSchema: null, connectionString: connectionString, - accessToken: CONFIG_TOKEN); + accessToken: CONFIG_TOKEN, + new()); postgreSqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 4601c99b61..69403c00cd 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -78,18 +78,18 @@ public void CheckConfigEnvParsingTest( if (replaceEnvVar) { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), + GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)), "Should read the expected config"); } else { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), + GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)), "Should read the expected config"); } Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), + GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)), "Should read actual config"); Assert.AreEqual(expectedConfig.ToJson(), actualConfig.ToJson()); } @@ -129,7 +129,7 @@ public void TestConfigParsingWithEnvVarReplacement(bool replaceEnvVar, string da string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceConfigForGivenDatabase(databaseType)); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -177,7 +177,7 @@ public void TestConfigParsingWhenDataSourceOptionsForCosmosDBContainsInvalidValu string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceOptionsForCosmosDBWithInvalidValues()); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true)); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -320,7 +320,7 @@ public void TestDataSourceDeserializationFailures(string dbType, string connecti ""entities"":{ } }"; // replaceEnvVar: true is needed to make sure we do post-processing for the connection string case - Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true))); + Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true))); Assert.IsNull(deserializedConfig); } diff --git a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs index 2b62c6b444..4f6729e47d 100644 --- a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs @@ -115,7 +115,8 @@ await provider.Initialize( provider.GetConfig().ToJson(), graphQLSchema: null, connectionString: connectionString, - accessToken: CONFIG_TOKEN); + accessToken: CONFIG_TOKEN, + new()); msSqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } From fa6582b365f356ed8d18f453dd8dabf67c6ef4aa Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 31 Oct 2025 09:47:27 -0700 Subject: [PATCH 10/37] format, cleanup --- .../AzureKeyVaultOptionsConverterFactory.cs | 28 +++++++++++--- ...erializationVariableReplacementSettings.cs | 16 ++++---- src/Config/FileSystemRuntimeConfigLoader.cs | 8 +++- .../ObjectModel/AzureKeyVaultOptions.cs | 37 +++++++++++++++++++ src/Config/RuntimeConfigLoader.cs | 12 ++---- .../Configuration/ConfigurationTests.cs | 18 ++++----- .../UnitTests/MySqlQueryExecutorUnitTests.cs | 2 +- .../PostgreSqlQueryExecutorUnitTests.cs | 2 +- .../UnitTests/SqlQueryExecutorUnitTests.cs | 2 +- 9 files changed, 88 insertions(+), 37 deletions(-) diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs index b6e70f05b6..7ccfbf5c06 100644 --- a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -67,11 +67,7 @@ public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? { if (reader.TokenType is JsonTokenType.EndObject) { - return new AzureKeyVaultOptions - { - Endpoint = endpoint, - RetryPolicy = retryPolicy - }; + return new AzureKeyVaultOptions(endpoint, retryPolicy); } string? property = reader.GetString(); @@ -105,9 +101,29 @@ public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? throw new JsonException("Invalid AzureKeyVaultOptions format"); } + /// + /// When writing the AzureKeyVaultOptions back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// public override void Write(Utf8JsonWriter writer, AzureKeyVaultOptions value, JsonSerializerOptions options) { - JsonSerializer.Serialize(writer, value, options); + writer.WriteStartObject(); + + if (value?.UserProvidedEndpoint is true) + { + writer.WritePropertyName("endpoint"); + JsonSerializer.Serialize(writer, value.Endpoint, options); + } + + if (value?.UserProvidedRetryPolicy is true) + { + writer.WritePropertyName("retry-policy"); + JsonSerializer.Serialize(writer, value.RetryPolicy, options); + } + + writer.WriteEndObject(); } } } diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index 198b9a1a65..58905d1045 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -100,11 +100,11 @@ private string ReplaceAkvVariable(Match match) { // strips first and last characters, ie: '''hello'' --> ''hello' string name = Regex.Match(match.Value, INNER_AKV_PATTERN).Value[1..^1]; - string? value = GetAKVVariable(name); + string? value = GetAkvVariable(name); if (EnvFailureMode == EnvironmentVariableReplacementFailureMode.Throw) { return value is not null ? value : - throw new DataApiBuilderException(message: $"Azure Key Vault Variable, {name}, not found.", + throw new DataApiBuilderException(message: $"Azure Key Vault Variable, '{name}', not found.", statusCode: System.Net.HttpStatusCode.ServiceUnavailable, subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } @@ -119,7 +119,7 @@ private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) if (string.IsNullOrWhiteSpace(options.Endpoint)) { throw new DataApiBuilderException( - "Azure Key Vault endpoint must be specified.", + "Missing 'endpoint' property is required to connect to Azure Key Value.", System.Net.HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } @@ -138,16 +138,16 @@ private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) }; clientOptions.Retry.Mode = retryMode; - clientOptions.Retry.MaxRetries = options.RetryPolicy.MaxCount ?? 3; - clientOptions.Retry.Delay = TimeSpan.FromSeconds(options.RetryPolicy.DelaySeconds ?? 1); - clientOptions.Retry.MaxDelay = TimeSpan.FromSeconds(options.RetryPolicy.MaxDelaySeconds ?? 16); - clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(options.RetryPolicy.NetworkTimeoutSeconds ?? 30); + clientOptions.Retry.MaxRetries = options.RetryPolicy.MaxCount ?? AKVRetryPolicyOptions.DEFAULT_MAX_COUNT; + clientOptions.Retry.Delay = TimeSpan.FromSeconds(options.RetryPolicy.DelaySeconds ?? AKVRetryPolicyOptions.DEFAULT_DELAY_SECONDS); + clientOptions.Retry.MaxDelay = TimeSpan.FromSeconds(options.RetryPolicy.MaxDelaySeconds ?? AKVRetryPolicyOptions.DEFAULT_MAX_DELAY_SECONDS); + clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(options.RetryPolicy.NetworkTimeoutSeconds ?? AKVRetryPolicyOptions.DEFAULT_NETWORK_TIMEOUT_SECONDS); } return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions); } - private string? GetAKVVariable(string name) + private string? GetAkvVariable(string name) { if (_akvClient is null) { diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 184b53aa49..6da504e8b8 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -226,8 +226,12 @@ public bool TryLoadConfig( } } - if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, - new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true), logger: null, connectionString: _connectionString)) + if (!string.IsNullOrEmpty(json) && TryParseConfig( + json, + out RuntimeConfig, + new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true), + logger: null, + connectionString: _connectionString)) { if (TrySetupConfigFileWatcher()) { diff --git a/src/Config/ObjectModel/AzureKeyVaultOptions.cs b/src/Config/ObjectModel/AzureKeyVaultOptions.cs index 27094cd16f..ebd1e909c1 100644 --- a/src/Config/ObjectModel/AzureKeyVaultOptions.cs +++ b/src/Config/ObjectModel/AzureKeyVaultOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Azure.DataApiBuilder.Config.ObjectModel; @@ -12,4 +13,40 @@ public record AzureKeyVaultOptions [JsonPropertyName("retry-policy")] public AKVRetryPolicyOptions? RetryPolicy { get; init; } + + /// + /// Flag which informs CLI and JSON serializer whether to write endpoint + /// property and value to the runtime config file. + /// When user doesn't provide the endpoint property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Endpoint))] + public bool UserProvidedEndpoint { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write retry-policy + /// property and value to the runtime config file. + /// When user doesn't provide the retry-policy property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(RetryPolicy))] + public bool UserProvidedRetryPolicy { get; init; } = false; + + [JsonConstructor] + public AzureKeyVaultOptions(string? endpoint = null, AKVRetryPolicyOptions? retryPolicy = null) + { + if (endpoint is not null) + { + Endpoint = endpoint; + UserProvidedEndpoint = true; + } + + if (retryPolicy is not null) + { + RetryPolicy = retryPolicy; + UserProvidedRetryPolicy = true; + } + } } diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 7b7ff8b646..80cd3680bd 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -51,7 +51,7 @@ public RuntimeConfigLoader(HotReloadEventHandler? handler = /// DabChangeToken #pragma warning disable CA1024 // Use properties where appropriate public IChangeToken GetChangeToken() -#pragma warning restore CA1024 // Use properties where Appropriate +#pragma warning restore CA1024 // Use properties where appropriate { return _changeToken; } @@ -138,7 +138,7 @@ protected void SignalConfigChanged(string message = "") /// AzureKeyVaultOptions if present, null otherwise. private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions(string json, bool enableEnvReplacement, - Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) + EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) { JsonSerializerOptions options = new() { @@ -156,12 +156,6 @@ protected void SignalConfigChanged(string message = "") options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings: envOnlySettings)); options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings: envOnlySettings)); - // Add environment variable replacement if enabled - if (enableEnvReplacement) - { - - } - try { using JsonDocument doc = JsonDocument.Parse(json); @@ -195,7 +189,7 @@ public static bool TryParseConfig(string json, string? connectionString = null) { // First pass: extract AzureKeyVault options if AKV replacement is requested - if (replacementSettings?.DoReplaceAkvVar == true) + if (replacementSettings?.DoReplaceAkvVar is true) { AzureKeyVaultOptions? azureKeyVaultOptions = ExtractAzureKeyVaultOptions( json, diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 3a6fb33fe3..05764404d8 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -838,9 +838,9 @@ public void MsSqlConnStringSupplementedWithAppNameProperty( // Act bool configParsed = RuntimeConfigLoader.TryParseConfig( - runtimeConfig.ToJson(), - out RuntimeConfig updatedRuntimeConfig, - new ()); + json: runtimeConfig.ToJson(), + config: out RuntimeConfig updatedRuntimeConfig, + replacementSettings: new()); // Assert Assert.AreEqual( @@ -891,9 +891,9 @@ public void PgSqlConnStringSupplementedWithAppNameProperty( // Act bool configParsed = RuntimeConfigLoader.TryParseConfig( - runtimeConfig.ToJson(), - out RuntimeConfig updatedRuntimeConfig, - new ()); + json: runtimeConfig.ToJson(), + config: out RuntimeConfig updatedRuntimeConfig, + replacementSettings: new()); // Assert Assert.AreEqual( @@ -956,9 +956,9 @@ public void TestConnectionStringIsCorrectlyUpdatedWithApplicationName( // Act bool configParsed = RuntimeConfigLoader.TryParseConfig( - runtimeConfig.ToJson(), - out RuntimeConfig updatedRuntimeConfig, - new ()); + json: runtimeConfig.ToJson(), + config: out RuntimeConfig updatedRuntimeConfig, + replacementSettings: new()); // Assert Assert.AreEqual( diff --git a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs index df92090dac..63deed78d3 100644 --- a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs @@ -82,7 +82,7 @@ await provider.Initialize( graphQLSchema: null, connectionString: connectionString, accessToken: CONFIG_TOKEN, - new()); + replacementSettings: new()); mySqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } diff --git a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs index a0b0b34108..6039c46a72 100644 --- a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs @@ -90,7 +90,7 @@ await provider.Initialize( graphQLSchema: null, connectionString: connectionString, accessToken: CONFIG_TOKEN, - new()); + replacementSettings: new()); postgreSqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } diff --git a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs index 4f6729e47d..b3782950f9 100644 --- a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs @@ -116,7 +116,7 @@ await provider.Initialize( graphQLSchema: null, connectionString: connectionString, accessToken: CONFIG_TOKEN, - new()); + replacementSettings: new()); msSqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } From b7613332d8a6458ade086e9fb90c27875f509f05 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:12:38 -0700 Subject: [PATCH 11/37] Update src/Config/RuntimeConfigLoader.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/RuntimeConfigLoader.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 80cd3680bd..d75ace3ddd 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -310,10 +310,6 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new EntitySourceConverterFactory(replacementSettings)); options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replacementSettings)); options.Converters.Add(new EntityRestOptionsConverterFactory(replacementSettings)); - options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replacementSettings)); - options.Converters.Add(new EntitySourceConverterFactory(replacementSettings)); - options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replacementSettings)); - options.Converters.Add(new EntityRestOptionsConverterFactory(replacementSettings)); options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); From 9411180da240bdf8fba7853e1544a4b1c9e57016 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:12:54 -0700 Subject: [PATCH 12/37] Update src/Config/DeserializationVariableReplacementSettings.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/DeserializationVariableReplacementSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index 58905d1045..e12200b896 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -119,7 +119,7 @@ private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) if (string.IsNullOrWhiteSpace(options.Endpoint)) { throw new DataApiBuilderException( - "Missing 'endpoint' property is required to connect to Azure Key Value.", + "Missing 'endpoint' property is required to connect to Azure Key Vault.", System.Net.HttpStatusCode.InternalServerError, DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } From 890e2c64948a405af23e1c33a96a108b1c337855 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:13:20 -0700 Subject: [PATCH 13/37] Update src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Converters/DatasourceHealthOptionsConvertorFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs index af1fd381aa..976e75720b 100644 --- a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class DataSourceHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private DeserializationVariableReplacementSettings? _replacementSettings; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) From 9af3a4cc1b3b3679f54d9c09a2d0f74148d62baf Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:13:35 -0700 Subject: [PATCH 14/37] Update src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Converters/DatasourceHealthOptionsConvertorFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs index 976e75720b..d8286ff7a0 100644 --- a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs @@ -36,7 +36,7 @@ private class HealthCheckOptionsConverter : JsonConverterWhether to replace environment variable with its /// value or not while deserializing. From 11318c6a1c1ad7c08fca718b1a450d274c38c0f2 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:13:52 -0700 Subject: [PATCH 15/37] Update src/Config/Converters/EntityCacheOptionsConverterFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/EntityCacheOptionsConverterFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs index 175cdae937..82ce09ece6 100644 --- a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs @@ -38,7 +38,7 @@ private class EntityCacheOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private DeserializationVariableReplacementSettings? _replacementSettings; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// The replacement settings to use while deserializing. public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) From b9ef8f9fddea068a6d9ce35a72e0119c2f5f2de1 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:14:12 -0700 Subject: [PATCH 16/37] Update src/Config/Converters/EntityCacheOptionsConverterFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/EntityCacheOptionsConverterFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs index 82ce09ece6..641efd062f 100644 --- a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs @@ -14,7 +14,7 @@ internal class EntityCacheOptionsConverterFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private DeserializationVariableReplacementSettings? _replacementSettings; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) From 3b1d6827e84190b3f08f2ff75737233510f83526 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:18:11 -0700 Subject: [PATCH 17/37] Update src/Config/Converters/StringJsonConverterFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/StringJsonConverterFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/StringJsonConverterFactory.cs b/src/Config/Converters/StringJsonConverterFactory.cs index 102e01d628..c3f5333237 100644 --- a/src/Config/Converters/StringJsonConverterFactory.cs +++ b/src/Config/Converters/StringJsonConverterFactory.cs @@ -13,7 +13,7 @@ namespace Azure.DataApiBuilder.Config.Converters; /// public class StringJsonConverterFactory : JsonConverterFactory { - private DeserializationVariableReplacementSettings _replacementSettings; + private readonly DeserializationVariableReplacementSettings _replacementSettings; public StringJsonConverterFactory(DeserializationVariableReplacementSettings replacementSettings) { From 15abe47fd4e95381ce2c80fd28aaf407ab2b618a Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:18:35 -0700 Subject: [PATCH 18/37] Update src/Config/Converters/McpRuntimeOptionsConverterFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/McpRuntimeOptionsConverterFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs index 4143cb395a..d75cbbef5a 100644 --- a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs @@ -37,7 +37,7 @@ private class McpRuntimeOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private DeserializationVariableReplacementSettings? _replacementSettings; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. From da9d9742d6a960127137ae890298a1653c98350d Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:20:09 -0700 Subject: [PATCH 19/37] Update src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs index 2cfcd720e9..85213d12c1 100644 --- a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class RuntimeHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private DeserializationVariableReplacementSettings? _replacementSettings; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) From 3bde74c69ac91c031155eff1eb4eb820360b0ff3 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:20:26 -0700 Subject: [PATCH 20/37] Update src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs index 85213d12c1..9c5f46dce2 100644 --- a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs @@ -34,7 +34,7 @@ private class HealthCheckOptionsConverter : JsonConverterWhether to replace environment variable with its /// value or not while deserializing. From 025cbc349c6e421f23ad9da540b1f3fb6773c31d Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 31 Oct 2025 10:31:43 -0700 Subject: [PATCH 21/37] format --- src/Config/RuntimeConfigLoader.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index d75ace3ddd..e7509664f4 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -136,7 +136,8 @@ protected void SignalConfigChanged(string message = "") /// Whether to enable environment variable replacement during extraction. /// Failure mode for environment variable replacement if enabled. /// AzureKeyVaultOptions if present, null otherwise. - private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions(string json, + private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions( + string json, bool enableEnvReplacement, EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) { @@ -192,10 +193,10 @@ public static bool TryParseConfig(string json, if (replacementSettings?.DoReplaceAkvVar is true) { AzureKeyVaultOptions? azureKeyVaultOptions = ExtractAzureKeyVaultOptions( - json, + json: json, enableEnvReplacement: replacementSettings.DoReplaceEnvVar, replacementFailureMode: replacementSettings.EnvFailureMode); - + // Update replacement settings with the extracted AKV options if (azureKeyVaultOptions is not null) { @@ -323,7 +324,7 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings)); options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings)); options.Converters.Add(new FileSinkConverter(replacementSettings)); - + // Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings)); From 1c6f02097fc36b48054f890e49a3dc9fb2a1ef6d Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 31 Oct 2025 10:52:36 -0700 Subject: [PATCH 22/37] remove last vestigates of replaceEnvVar --- src/Cli.Tests/EndToEndTests.cs | 27 ++++++++++------- src/Cli/Exporter.cs | 12 +++++--- src/Config/FileSystemRuntimeConfigLoader.cs | 30 ++++++++++++------- src/Config/ObjectModel/RuntimeConfig.cs | 14 ++++++--- ...untimeConfigLoaderJsonDeserializerTests.cs | 3 +- 5 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 7fe017501f..a78aa5d5fd 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -116,21 +116,25 @@ public void TestInitializingRestAndGraphQLGlobalSettings() string[] args = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--connection-string", SAMPLE_TEST_CONN_STRING, "--database-type", "mssql", "--rest.path", "/rest-api", "--rest.enabled", "false", "--graphql.path", "/graphql-api" }; Program.Execute(args, _cliLogger!, _fileSystem!, _runtimeConfigLoader!); + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig( TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig, - replaceEnvVar: true)); + replacementSettings: replacementSettings)); - SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource.ConnectionString); - Assert.AreEqual(ProductInfo.GetDataApiBuilderUserAgent(), builder.ApplicationName); + if (runtimeConfig is not null) + { + SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource.ConnectionString); + Assert.AreEqual(ProductInfo.GetDataApiBuilderUserAgent(), builder.ApplicationName); - Assert.IsNotNull(runtimeConfig); - Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType); - Assert.IsNotNull(runtimeConfig.Runtime); - Assert.AreEqual("/rest-api", runtimeConfig.Runtime.Rest?.Path); - Assert.IsFalse(runtimeConfig.Runtime.Rest?.Enabled); - Assert.AreEqual("/graphql-api", runtimeConfig.Runtime.GraphQL?.Path); - Assert.IsTrue(runtimeConfig.Runtime.GraphQL?.Enabled); + Assert.IsNotNull(runtimeConfig); + Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType); + Assert.IsNotNull(runtimeConfig.Runtime); + Assert.AreEqual("/rest-api", runtimeConfig.Runtime.Rest?.Path); + Assert.IsFalse(runtimeConfig.Runtime.Rest?.Enabled); + Assert.AreEqual("/graphql-api", runtimeConfig.Runtime.GraphQL?.Path); + Assert.IsTrue(runtimeConfig.Runtime.GraphQL?.Enabled); + } } /// @@ -195,10 +199,11 @@ public void TestEnablingMultipleCreateOperation(CliBool isMultipleCreateEnabled, Program.Execute(args.ToArray(), _cliLogger!, _fileSystem!, _runtimeConfigLoader!); + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig( TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig, - replaceEnvVar: true)); + replacementSettings: replacementSettings)); Assert.IsNotNull(runtimeConfig); Assert.AreEqual(expectedDbType, runtimeConfig.DataSource.DatabaseType); diff --git a/src/Cli/Exporter.cs b/src/Cli/Exporter.cs index d4f103e868..cd9eef04d1 100644 --- a/src/Cli/Exporter.cs +++ b/src/Cli/Exporter.cs @@ -44,7 +44,8 @@ public static bool Export(ExportOptions options, ILogger logger, FileSystemRunti } // Load the runtime configuration from the file - if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig, replaceEnvVar: true)) + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig, replacementSettings: replacementSettings)) { logger.LogError("Failed to read the config file: {0}.", runtimeConfigFile); return false; @@ -62,9 +63,12 @@ public static bool Export(ExportOptions options, ILogger logger, FileSystemRunti { try { - ExportGraphQL(options, runtimeConfig, fileSystem, loader, logger).Wait(); - isSuccess = true; - break; + if (runtimeConfig is not null) + { + ExportGraphQL(options, runtimeConfig, fileSystem, loader, logger).Wait(); + isSuccess = true; + break; + } } catch { diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 6da504e8b8..c5ecfd6de4 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -172,7 +172,7 @@ private void OnNewFileContentsDetected(object? sender, EventArgs e) catch (Exception ex) { // Need to remove the dependencies in startup on the RuntimeConfigProvider - // before we can have an ILogger here. + // before we can have anILogger here. Console.WriteLine("Unable to hot reload configuration file due to " + ex.Message); } } @@ -182,17 +182,16 @@ private void OnNewFileContentsDetected(object? sender, EventArgs e) /// /// The path to the dab-config.json file. /// The loaded RuntimeConfig, or null if none was loaded. - /// Whether to replace environment variable with its - /// value or not while deserializing. /// ILogger for logging errors. /// When not null indicates we need to overwrite mode and how to do so. + /// Settings for variable replacement during deserialization. If null, uses default settings with environment variable replacement enabled. /// True if the config was loaded, otherwise false. public bool TryLoadConfig( string path, [NotNullWhen(true)] out RuntimeConfig? config, - bool replaceEnvVar = false, ILogger? logger = null, - bool? isDevMode = null) + bool? isDevMode = null, + DeserializationVariableReplacementSettings? replacementSettings = null) { if (_fileSystem.File.Exists(path)) { @@ -226,10 +225,13 @@ public bool TryLoadConfig( } } + // Use default replacement settings if none provided + replacementSettings ??= new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + if (!string.IsNullOrEmpty(json) && TryParseConfig( json, out RuntimeConfig, - new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true), + replacementSettings, logger: null, connectionString: _connectionString)) { @@ -297,12 +299,16 @@ public bool TryLoadConfig( /// Tries to load the config file using the filename known to the RuntimeConfigLoader and for the default environment. /// /// The loaded RuntimeConfig, or null if none was loaded. - /// Whether to replace environment variable with its - /// value or not while deserializing. + /// Settings for variable replacement during deserialization. If null, uses default settings with environment variable replacement disabled. /// True if the config was loaded, otherwise false. public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false) { - return TryLoadConfig(ConfigFilePath, out config, replaceEnvVar); + // Convert legacy replaceEnvVar parameter to replacement settings for backward compatibility + DeserializationVariableReplacementSettings? replacementSettings = replaceEnvVar + ? new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true) + : new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAkvVar: false); + + return TryLoadConfig(ConfigFilePath, out config, replacementSettings: replacementSettings); } /// @@ -312,7 +318,11 @@ public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? c private void HotReloadConfig(bool isDevMode, ILogger? logger = null) { logger?.LogInformation(message: "Starting hot-reload process for config: {ConfigFilePath}", ConfigFilePath); - if (!TryLoadConfig(ConfigFilePath, out _, replaceEnvVar: true, isDevMode: isDevMode)) + + // Use default replacement settings for hot reload + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + + if (!TryLoadConfig(ConfigFilePath, out _, logger: logger, isDevMode: isDevMode, replacementSettings: replacementSettings)) { throw new DataApiBuilderException( message: "Deserialization of the configuration file failed.", diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 253271553e..6fb7808d82 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -298,13 +298,19 @@ public RuntimeConfig( foreach (string dataSourceFile in DataSourceFiles.SourceFiles) { - if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replaceEnvVar: true)) + // Use default replacement settings for environment variable replacement + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + + if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings)) { try { - _dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - _entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - allEntities = allEntities.Concat(config.Entities.AsEnumerable()); + if (config is not null) + { + _dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + _entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + allEntities = allEntities.Concat(config.Entities.AsEnumerable()); + } } catch (Exception e) { diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 69403c00cd..078ac5c95c 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -339,7 +339,8 @@ public void TestLoadRuntimeConfigFailures( MockFileSystem fileSystem = new(); FileSystemRuntimeConfigLoader loader = new(fileSystem); - Assert.IsFalse(loader.TryLoadConfig(configFileName, out RuntimeConfig _)); + // Use null replacement settings for this test + Assert.IsFalse(loader.TryLoadConfig(configFileName, out RuntimeConfig _, replacementSettings: null)); } /// From 9a14dc3ce20f2ce4099eab0ec7954f7aaeb7193a Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 31 Oct 2025 11:02:34 -0700 Subject: [PATCH 23/37] format --- src/Config/FileSystemRuntimeConfigLoader.cs | 8 ++++---- src/Config/ObjectModel/RuntimeConfig.cs | 2 +- src/Config/RuntimeConfigLoader.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index c5ecfd6de4..9f89e402e2 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -304,10 +304,10 @@ public bool TryLoadConfig( public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false) { // Convert legacy replaceEnvVar parameter to replacement settings for backward compatibility - DeserializationVariableReplacementSettings? replacementSettings = replaceEnvVar + DeserializationVariableReplacementSettings? replacementSettings = replaceEnvVar ? new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true) : new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAkvVar: false); - + return TryLoadConfig(ConfigFilePath, out config, replacementSettings: replacementSettings); } @@ -318,10 +318,10 @@ public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? c private void HotReloadConfig(bool isDevMode, ILogger? logger = null) { logger?.LogInformation(message: "Starting hot-reload process for config: {ConfigFilePath}", ConfigFilePath); - + // Use default replacement settings for hot reload DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); - + if (!TryLoadConfig(ConfigFilePath, out _, logger: logger, isDevMode: isDevMode, replacementSettings: replacementSettings)) { throw new DataApiBuilderException( diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 6fb7808d82..3d7cf6fb08 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -300,7 +300,7 @@ public RuntimeConfig( { // Use default replacement settings for environment variable replacement DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); - + if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings)) { try diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index e7509664f4..bad5aa8680 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -193,7 +193,7 @@ public static bool TryParseConfig(string json, if (replacementSettings?.DoReplaceAkvVar is true) { AzureKeyVaultOptions? azureKeyVaultOptions = ExtractAzureKeyVaultOptions( - json: json, + json: json, enableEnvReplacement: replacementSettings.DoReplaceEnvVar, replacementFailureMode: replacementSettings.EnvFailureMode); From 4f38f929990ebf67e210b15e381eea5f95a1c21f Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 31 Oct 2025 11:12:18 -0700 Subject: [PATCH 24/37] format --- src/Cli.Tests/EnvironmentTests.cs | 2 +- src/Config/DeserializationVariableReplacementSettings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cli.Tests/EnvironmentTests.cs b/src/Cli.Tests/EnvironmentTests.cs index 08653aaa41..2d6378cf74 100644 --- a/src/Cli.Tests/EnvironmentTests.cs +++ b/src/Cli.Tests/EnvironmentTests.cs @@ -24,7 +24,7 @@ public void TestInitialize() doReplaceEnvVar: true, doReplaceAkvVar: false, envFailureMode: EnvironmentVariableReplacementFailureMode.Throw); - + StringJsonConverterFactory converterFactory = new(replacementSettings); _options = new() { diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index e12200b896..4622ae0a40 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Text.RegularExpressions; using Azure.Core; using Azure.Identity; using Azure.Security.KeyVault.Secrets; using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; +using System.Text.RegularExpressions; namespace Azure.DataApiBuilder.Config { From 4742669e22e40387349f3ae54c0a6cf0ce48fc7e Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 31 Oct 2025 12:58:43 -0700 Subject: [PATCH 25/37] using ordering --- src/Config/DeserializationVariableReplacementSettings.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index 4622ae0a40..a644465093 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.RegularExpressions; using Azure.Core; -using Azure.Identity; -using Azure.Security.KeyVault.Secrets; using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; -using System.Text.RegularExpressions; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; namespace Azure.DataApiBuilder.Config { From 10855d005ca45a34b7e8e539aa38a1168cfd6c53 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Tue, 4 Nov 2025 14:33:39 -0800 Subject: [PATCH 26/37] removed unused null check --- src/Cli.Tests/ConfigureOptionsTests.cs | 56 +++++++++++++------------- src/Cli.Tests/EndToEndTests.cs | 21 +++++----- src/Cli/Exporter.cs | 9 ++--- 3 files changed, 40 insertions(+), 46 deletions(-) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index 073f349a67..9bce3a5fee 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -95,7 +95,7 @@ public void TestAddDepthLimitForGraphQL() Assert.IsTrue(_fileSystem!.File.Exists(TEST_RUNTIME_CONFIG_FILE)); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNull(config.Runtime!.GraphQL!.DepthLimit); // Act: Attmepts to Add Depth Limit @@ -108,7 +108,7 @@ public void TestAddDepthLimitForGraphQL() // Assert: Validate the Depth Limit is added Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config, replacementSettings: null)); Assert.IsNotNull(config.Runtime?.GraphQL?.DepthLimit); Assert.AreEqual(maxDepthLimit, config.Runtime.GraphQL.DepthLimit); } @@ -139,7 +139,7 @@ public void TestAddAKVOptions() // Assert: Validate the AKV options are added. Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNotNull(config.AzureKeyVault); Assert.IsNotNull(config.AzureKeyVault?.RetryPolicy); Assert.AreEqual("foo", config.AzureKeyVault?.Endpoint); @@ -177,7 +177,7 @@ public void TestAddAzureLogAnalyticsOptions() // Assert: Validate the Azure Log Analytics options are added. Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNotNull(config.Runtime); Assert.IsNotNull(config.Runtime.Telemetry); Assert.IsNotNull(config.Runtime.Telemetry.AzureLogAnalytics); @@ -222,7 +222,7 @@ public void TestAddFileSinkOptions() // Assert: Validate the file options are added. Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNotNull(config.Runtime); Assert.IsNotNull(config.Runtime.Telemetry); Assert.IsNotNull(config.Runtime.Telemetry.File); @@ -255,7 +255,7 @@ public void TestUpdateEnabledForGraphQLSettings(bool updatedEnabledValue) // Assert: Validate the Enabled Flag is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.Enabled); Assert.AreEqual(updatedEnabledValue, runtimeConfig.Runtime.GraphQL.Enabled); } @@ -283,7 +283,7 @@ public void TestUpdatePathForGraphQLSettings(string updatedPathValue) // Assert: Validate the Path update is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.Path); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.GraphQL.Path); } @@ -311,7 +311,7 @@ public void TestUpdateAllowIntrospectionForGraphQLSettings(bool updatedAllowIntr // Assert: Validate the Allow-Introspection value is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.AllowIntrospection); Assert.AreEqual(updatedAllowIntrospectionValue, runtimeConfig.Runtime.GraphQL.AllowIntrospection); } @@ -339,7 +339,7 @@ public void TestUpdateMultipleMutationCreateEnabledForGraphQLSettings(bool updat // Assert: Validate the Multiple-Mutation.Create.Enabled is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.MultipleMutationOptions?.MultipleCreateOptions?.Enabled); Assert.AreEqual(updatedMultipleMutationsCreateEnabledValue, runtimeConfig.Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions.Enabled); } @@ -368,7 +368,7 @@ public void TestUpdateMultipleParametersForGraphQLSettings() // Assert: Validate the path is updated and allow introspection is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.Path); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.AllowIntrospection); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.GraphQL.Path); @@ -397,7 +397,7 @@ public void TestUpdateEnabledForRestSettings(bool updatedEnabledValue) // Assert: Validate the Enabled Flag is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Enabled); Assert.AreEqual(updatedEnabledValue, runtimeConfig.Runtime.Rest.Enabled); } @@ -425,7 +425,7 @@ public void TestUpdatePathForRestSettings(string updatedPathValue) // Assert: Validate the Path update is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Path); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.Rest.Path); } @@ -452,7 +452,7 @@ public void TestUpdateRequestBodyStrictForRestSettings(bool updatedRequestBodySt // Assert: Validate the RequestBodyStrict Value is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.RequestBodyStrict); Assert.AreEqual(updatedRequestBodyStrictValue, runtimeConfig.Runtime.Rest.RequestBodyStrict); } @@ -480,7 +480,7 @@ public void TestUpdateMultipleParametersRestSettings(bool updatedEnabledValue, s // Assert: Validate the path is updated and enabled is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Path); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Enabled); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.Rest.Path); @@ -509,7 +509,7 @@ public void TestUpdateEnabledForCacheSettings(bool updatedEnabledValue) // Assert: Validate the cache Enabled Flag is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Cache?.Enabled); Assert.AreEqual(updatedEnabledValue, runtimeConfig.Runtime.Cache.Enabled); } @@ -535,7 +535,7 @@ public void TestUpdateTTLForCacheSettings(int updatedTtlValue) // Assert: Validate the TTL Value is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Cache?.TtlSeconds); Assert.AreEqual(updatedTtlValue, runtimeConfig.Runtime.Cache.TtlSeconds); } @@ -565,7 +565,7 @@ public void TestCaseInsensitiveUpdateModeForHostSettings(string modeValue) // Assert: Validate the Mode in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Mode); Assert.AreEqual(updatedModeValue, runtimeConfig.Runtime.Host.Mode); } @@ -594,7 +594,7 @@ public void TestUpdateCorsOriginsForHostSettings(string inputValue) // Assert: Validate the Cors.Origins in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Cors?.Origins); CollectionAssert.AreEqual(originsValue.ToArray(), runtimeConfig.Runtime.Host.Cors.Origins); } @@ -621,7 +621,7 @@ public void TestUpdateCorsAllowCredentialsHostSettings(bool allowCredentialsValu // Assert: Validate the cors.allow-credentials in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Cors?.AllowCredentials); Assert.AreEqual(allowCredentialsValue, runtimeConfig.Runtime.Host.Cors.AllowCredentials); } @@ -650,7 +650,7 @@ public void TestUpdateAuthenticationProviderHostSettings(string authenticationPr // Assert: Validate the authentication.provider in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Authentication?.Provider); Assert.AreEqual(authenticationProviderValue, runtimeConfig.Runtime.Host.Authentication.Provider); } @@ -676,7 +676,7 @@ public void TestUpdateAuthenticationJwtAudienceHostSettings(string updatedJwtAud // Assert: Validate the authentication.jwt.audience in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Authentication?.Jwt?.Audience); Assert.AreEqual(updatedJwtAudienceValue.ToString(), runtimeConfig.Runtime.Host.Authentication.Jwt.Audience); } @@ -702,7 +702,7 @@ public void TestUpdateAuthenticationJwtIssuerHostSettings(string updatedJwtIssue // Assert: Validate the authentication.jwt.issuer in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Authentication?.Jwt?.Issuer); Assert.AreEqual(updatedJwtIssuerValue.ToString(), runtimeConfig.Runtime.Host.Authentication.Jwt.Issuer); } @@ -720,7 +720,7 @@ public void TestUpdateDepthLimitForGraphQL(int? newDepthLimit) int currentDepthLimit = 8; // Arrange - RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config); + RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config, replacementSettings: null); Assert.IsNotNull(config); config = config with { @@ -745,7 +745,7 @@ public void TestUpdateDepthLimitForGraphQL(int? newDepthLimit) // Assert: Validate the Depth Limit is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config, replacementSettings: null)); Assert.AreEqual(newDepthLimit, config.Runtime?.GraphQL?.DepthLimit); } @@ -779,7 +779,7 @@ public void TestDatabaseTypeUpdate(string dbType) // Assert Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNotNull(config.Runtime); Assert.AreEqual(config.DataSource.DatabaseType, Enum.Parse(dbType, ignoreCase: true)); } @@ -809,7 +809,7 @@ public void TestDatabaseTypeUpdateCosmosDB_NoSQLToMSSQL() // Assert Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNotNull(config.Runtime); Assert.AreEqual(config.DataSource.DatabaseType, DatabaseType.MSSQL); Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("set-session-context", false), true); @@ -845,7 +845,7 @@ public void TestDatabaseTypeUpdateMSSQLToCosmosDB_NoSQL() // Assert Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNotNull(config.Runtime); Assert.AreEqual(config.DataSource.DatabaseType, DatabaseType.CosmosDB_NoSQL); Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("database"), "testdb"); @@ -938,7 +938,7 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig) Assert.IsTrue(_fileSystem!.File.Exists(TEST_RUNTIME_CONFIG_FILE)); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config, replacementSettings: null)); Assert.IsNotNull(config.Runtime); } } diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index a78aa5d5fd..5dbf97ca5e 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -122,19 +122,16 @@ public void TestInitializingRestAndGraphQLGlobalSettings() out RuntimeConfig? runtimeConfig, replacementSettings: replacementSettings)); - if (runtimeConfig is not null) - { - SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource.ConnectionString); - Assert.AreEqual(ProductInfo.GetDataApiBuilderUserAgent(), builder.ApplicationName); + SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource.ConnectionString); + Assert.AreEqual(ProductInfo.GetDataApiBuilderUserAgent(), builder.ApplicationName); - Assert.IsNotNull(runtimeConfig); - Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType); - Assert.IsNotNull(runtimeConfig.Runtime); - Assert.AreEqual("/rest-api", runtimeConfig.Runtime.Rest?.Path); - Assert.IsFalse(runtimeConfig.Runtime.Rest?.Enabled); - Assert.AreEqual("/graphql-api", runtimeConfig.Runtime.GraphQL?.Path); - Assert.IsTrue(runtimeConfig.Runtime.GraphQL?.Enabled); - } + Assert.IsNotNull(runtimeConfig); + Assert.AreEqual(DatabaseType.MSSQL, runtimeConfig.DataSource.DatabaseType); + Assert.IsNotNull(runtimeConfig.Runtime); + Assert.AreEqual("/rest-api", runtimeConfig.Runtime.Rest?.Path); + Assert.IsFalse(runtimeConfig.Runtime.Rest?.Enabled); + Assert.AreEqual("/graphql-api", runtimeConfig.Runtime.GraphQL?.Path); + Assert.IsTrue(runtimeConfig.Runtime.GraphQL?.Enabled); } /// diff --git a/src/Cli/Exporter.cs b/src/Cli/Exporter.cs index cd9eef04d1..896b485692 100644 --- a/src/Cli/Exporter.cs +++ b/src/Cli/Exporter.cs @@ -63,12 +63,9 @@ public static bool Export(ExportOptions options, ILogger logger, FileSystemRunti { try { - if (runtimeConfig is not null) - { - ExportGraphQL(options, runtimeConfig, fileSystem, loader, logger).Wait(); - isSuccess = true; - break; - } + ExportGraphQL(options, runtimeConfig, fileSystem, loader, logger).Wait(); + isSuccess = true; + break; } catch { From a172a16cc6119e080a77b3e8da7b51a55965532f Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Tue, 4 Nov 2025 20:23:29 -0800 Subject: [PATCH 27/37] address comments --- .../Converters/AzureKeyVaultOptionsConverterFactory.cs | 5 ++--- src/Config/FileSystemRuntimeConfigLoader.cs | 2 +- src/Config/ObjectModel/RuntimeConfig.cs | 9 +++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs index 7ccfbf5c06..92ed0c1a85 100644 --- a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -16,8 +16,7 @@ internal class AzureKeyVaultOptionsConverterFactory : JsonConverterFactory // value or not while deserializing. private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. + /// How to handle variable replacement during deserialization. internal AzureKeyVaultOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { _replacementSettings = replacementSettings; @@ -86,7 +85,7 @@ public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? case "retry-policy": if (reader.TokenType is JsonTokenType.StartObject) { - // Pass the replaceEnvVar setting to the retry policy converter + // Uses the AKVRetryPolicyOptionsConverter to read the retry-policy object. retryPolicy = JsonSerializer.Deserialize(ref reader, options); } diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 9f89e402e2..8f8f270609 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -172,7 +172,7 @@ private void OnNewFileContentsDetected(object? sender, EventArgs e) catch (Exception ex) { // Need to remove the dependencies in startup on the RuntimeConfigProvider - // before we can have anILogger here. + // before we can have an ILogger here. Console.WriteLine("Unable to hot reload configuration file due to " + ex.Message); } } diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 3d7cf6fb08..6896d82161 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -305,12 +305,9 @@ public RuntimeConfig( { try { - if (config is not null) - { - _dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - _entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - allEntities = allEntities.Concat(config.Entities.AsEnumerable()); - } + _dataSourceNameToDataSource = _dataSourceNameToDataSource.Concat(config._dataSourceNameToDataSource).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + _entityNameToDataSourceName = _entityNameToDataSourceName.Concat(config._entityNameToDataSourceName).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + allEntities = allEntities.Concat(config.Entities.AsEnumerable()); } catch (Exception e) { From 8b8d2def132cf699003ac551eb6f47b1b89ecaed Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Tue, 4 Nov 2025 20:30:21 -0800 Subject: [PATCH 28/37] default to false --- src/Config/DeserializationVariableReplacementSettings.cs | 4 ++-- src/Config/FileSystemRuntimeConfigLoader.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index a644465093..f675656521 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -52,8 +52,8 @@ public class DeserializationVariableReplacementSettings public DeserializationVariableReplacementSettings( AzureKeyVaultOptions? azureKeyVaultOptions = null, - bool doReplaceEnvVar = true, - bool doReplaceAkvVar = true, + bool doReplaceEnvVar = false, + bool doReplaceAkvVar = false, EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) { _azureKeyVaultOptions = azureKeyVaultOptions; diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 8f8f270609..4b429f301c 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -226,7 +226,7 @@ public bool TryLoadConfig( } // Use default replacement settings if none provided - replacementSettings ??= new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + replacementSettings ??= new DeserializationVariableReplacementSettings(); if (!string.IsNullOrEmpty(json) && TryParseConfig( json, From 250460ccb3944574aedb0b0fe12b538a8d0760c8 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 6 Nov 2025 09:38:09 -0800 Subject: [PATCH 29/37] fix unit test --- src/Cli.Tests/ConfigureOptionsTests.cs | 56 +++++++++++++------------- src/Cli/ConfigGenerator.cs | 22 +++++----- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/Cli.Tests/ConfigureOptionsTests.cs b/src/Cli.Tests/ConfigureOptionsTests.cs index 9bce3a5fee..073f349a67 100644 --- a/src/Cli.Tests/ConfigureOptionsTests.cs +++ b/src/Cli.Tests/ConfigureOptionsTests.cs @@ -95,7 +95,7 @@ public void TestAddDepthLimitForGraphQL() Assert.IsTrue(_fileSystem!.File.Exists(TEST_RUNTIME_CONFIG_FILE)); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config)); Assert.IsNull(config.Runtime!.GraphQL!.DepthLimit); // Act: Attmepts to Add Depth Limit @@ -108,7 +108,7 @@ public void TestAddDepthLimitForGraphQL() // Assert: Validate the Depth Limit is added Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config)); Assert.IsNotNull(config.Runtime?.GraphQL?.DepthLimit); Assert.AreEqual(maxDepthLimit, config.Runtime.GraphQL.DepthLimit); } @@ -139,7 +139,7 @@ public void TestAddAKVOptions() // Assert: Validate the AKV options are added. Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.AzureKeyVault); Assert.IsNotNull(config.AzureKeyVault?.RetryPolicy); Assert.AreEqual("foo", config.AzureKeyVault?.Endpoint); @@ -177,7 +177,7 @@ public void TestAddAzureLogAnalyticsOptions() // Assert: Validate the Azure Log Analytics options are added. Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.Runtime); Assert.IsNotNull(config.Runtime.Telemetry); Assert.IsNotNull(config.Runtime.Telemetry.AzureLogAnalytics); @@ -222,7 +222,7 @@ public void TestAddFileSinkOptions() // Assert: Validate the file options are added. Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.Runtime); Assert.IsNotNull(config.Runtime.Telemetry); Assert.IsNotNull(config.Runtime.Telemetry.File); @@ -255,7 +255,7 @@ public void TestUpdateEnabledForGraphQLSettings(bool updatedEnabledValue) // Assert: Validate the Enabled Flag is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.Enabled); Assert.AreEqual(updatedEnabledValue, runtimeConfig.Runtime.GraphQL.Enabled); } @@ -283,7 +283,7 @@ public void TestUpdatePathForGraphQLSettings(string updatedPathValue) // Assert: Validate the Path update is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.Path); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.GraphQL.Path); } @@ -311,7 +311,7 @@ public void TestUpdateAllowIntrospectionForGraphQLSettings(bool updatedAllowIntr // Assert: Validate the Allow-Introspection value is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.AllowIntrospection); Assert.AreEqual(updatedAllowIntrospectionValue, runtimeConfig.Runtime.GraphQL.AllowIntrospection); } @@ -339,7 +339,7 @@ public void TestUpdateMultipleMutationCreateEnabledForGraphQLSettings(bool updat // Assert: Validate the Multiple-Mutation.Create.Enabled is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.MultipleMutationOptions?.MultipleCreateOptions?.Enabled); Assert.AreEqual(updatedMultipleMutationsCreateEnabledValue, runtimeConfig.Runtime.GraphQL.MultipleMutationOptions.MultipleCreateOptions.Enabled); } @@ -368,7 +368,7 @@ public void TestUpdateMultipleParametersForGraphQLSettings() // Assert: Validate the path is updated and allow introspection is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.Path); Assert.IsNotNull(runtimeConfig.Runtime?.GraphQL?.AllowIntrospection); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.GraphQL.Path); @@ -397,7 +397,7 @@ public void TestUpdateEnabledForRestSettings(bool updatedEnabledValue) // Assert: Validate the Enabled Flag is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Enabled); Assert.AreEqual(updatedEnabledValue, runtimeConfig.Runtime.Rest.Enabled); } @@ -425,7 +425,7 @@ public void TestUpdatePathForRestSettings(string updatedPathValue) // Assert: Validate the Path update is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Path); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.Rest.Path); } @@ -452,7 +452,7 @@ public void TestUpdateRequestBodyStrictForRestSettings(bool updatedRequestBodySt // Assert: Validate the RequestBodyStrict Value is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.RequestBodyStrict); Assert.AreEqual(updatedRequestBodyStrictValue, runtimeConfig.Runtime.Rest.RequestBodyStrict); } @@ -480,7 +480,7 @@ public void TestUpdateMultipleParametersRestSettings(bool updatedEnabledValue, s // Assert: Validate the path is updated and enabled is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Path); Assert.IsNotNull(runtimeConfig.Runtime?.Rest?.Enabled); Assert.AreEqual(updatedPathValue, runtimeConfig.Runtime.Rest.Path); @@ -509,7 +509,7 @@ public void TestUpdateEnabledForCacheSettings(bool updatedEnabledValue) // Assert: Validate the cache Enabled Flag is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Cache?.Enabled); Assert.AreEqual(updatedEnabledValue, runtimeConfig.Runtime.Cache.Enabled); } @@ -535,7 +535,7 @@ public void TestUpdateTTLForCacheSettings(int updatedTtlValue) // Assert: Validate the TTL Value is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Cache?.TtlSeconds); Assert.AreEqual(updatedTtlValue, runtimeConfig.Runtime.Cache.TtlSeconds); } @@ -565,7 +565,7 @@ public void TestCaseInsensitiveUpdateModeForHostSettings(string modeValue) // Assert: Validate the Mode in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Mode); Assert.AreEqual(updatedModeValue, runtimeConfig.Runtime.Host.Mode); } @@ -594,7 +594,7 @@ public void TestUpdateCorsOriginsForHostSettings(string inputValue) // Assert: Validate the Cors.Origins in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Cors?.Origins); CollectionAssert.AreEqual(originsValue.ToArray(), runtimeConfig.Runtime.Host.Cors.Origins); } @@ -621,7 +621,7 @@ public void TestUpdateCorsAllowCredentialsHostSettings(bool allowCredentialsValu // Assert: Validate the cors.allow-credentials in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Cors?.AllowCredentials); Assert.AreEqual(allowCredentialsValue, runtimeConfig.Runtime.Host.Cors.AllowCredentials); } @@ -650,7 +650,7 @@ public void TestUpdateAuthenticationProviderHostSettings(string authenticationPr // Assert: Validate the authentication.provider in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Authentication?.Provider); Assert.AreEqual(authenticationProviderValue, runtimeConfig.Runtime.Host.Authentication.Provider); } @@ -676,7 +676,7 @@ public void TestUpdateAuthenticationJwtAudienceHostSettings(string updatedJwtAud // Assert: Validate the authentication.jwt.audience in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Authentication?.Jwt?.Audience); Assert.AreEqual(updatedJwtAudienceValue.ToString(), runtimeConfig.Runtime.Host.Authentication.Jwt.Audience); } @@ -702,7 +702,7 @@ public void TestUpdateAuthenticationJwtIssuerHostSettings(string updatedJwtIssue // Assert: Validate the authentication.jwt.issuer in Host is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? runtimeConfig)); Assert.IsNotNull(runtimeConfig.Runtime?.Host?.Authentication?.Jwt?.Issuer); Assert.AreEqual(updatedJwtIssuerValue.ToString(), runtimeConfig.Runtime.Host.Authentication.Jwt.Issuer); } @@ -720,7 +720,7 @@ public void TestUpdateDepthLimitForGraphQL(int? newDepthLimit) int currentDepthLimit = 8; // Arrange - RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config, replacementSettings: null); + RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? config); Assert.IsNotNull(config); config = config with { @@ -745,7 +745,7 @@ public void TestUpdateDepthLimitForGraphQL(int? newDepthLimit) // Assert: Validate the Depth Limit is updated Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out config)); Assert.AreEqual(newDepthLimit, config.Runtime?.GraphQL?.DepthLimit); } @@ -779,7 +779,7 @@ public void TestDatabaseTypeUpdate(string dbType) // Assert Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.Runtime); Assert.AreEqual(config.DataSource.DatabaseType, Enum.Parse(dbType, ignoreCase: true)); } @@ -809,7 +809,7 @@ public void TestDatabaseTypeUpdateCosmosDB_NoSQLToMSSQL() // Assert Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.Runtime); Assert.AreEqual(config.DataSource.DatabaseType, DatabaseType.MSSQL); Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("set-session-context", false), true); @@ -845,7 +845,7 @@ public void TestDatabaseTypeUpdateMSSQLToCosmosDB_NoSQL() // Assert Assert.IsTrue(isSuccess); string updatedConfig = _fileSystem!.File.ReadAllText(TEST_RUNTIME_CONFIG_FILE); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(updatedConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.Runtime); Assert.AreEqual(config.DataSource.DatabaseType, DatabaseType.CosmosDB_NoSQL); Assert.AreEqual(config.DataSource.Options!.GetValueOrDefault("database"), "testdb"); @@ -938,7 +938,7 @@ private void SetupFileSystemWithInitialConfig(string jsonConfig) Assert.IsTrue(_fileSystem!.File.Exists(TEST_RUNTIME_CONFIG_FILE)); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config, replacementSettings: null)); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? config)); Assert.IsNotNull(config.Runtime); } } diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 9a56f83c4a..7c35335089 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -2700,9 +2700,10 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( // Azure Key Vault Endpoint if (options.AzureKeyVaultEndpoint is not null) { + // Ensure endpoint flag is marked user provided so converter writes it. updatedAzureKeyVaultOptions = updatedAzureKeyVaultOptions is not null - ? updatedAzureKeyVaultOptions with { Endpoint = options.AzureKeyVaultEndpoint } - : new AzureKeyVaultOptions { Endpoint = options.AzureKeyVaultEndpoint }; + ? updatedAzureKeyVaultOptions with { Endpoint = options.AzureKeyVaultEndpoint, UserProvidedEndpoint = true } + : new AzureKeyVaultOptions(endpoint: options.AzureKeyVaultEndpoint); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.endpoint as '{endpoint}'", options.AzureKeyVaultEndpoint); } @@ -2711,7 +2712,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( { updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { Mode = options.AzureKeyVaultRetryPolicyMode.Value, UserProvidedMode = true } - : new AKVRetryPolicyOptions { Mode = options.AzureKeyVaultRetryPolicyMode.Value, UserProvidedMode = true }; + : new AKVRetryPolicyOptions(mode: options.AzureKeyVaultRetryPolicyMode.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.mode as '{mode}'", options.AzureKeyVaultRetryPolicyMode.Value); } @@ -2726,7 +2727,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { MaxCount = options.AzureKeyVaultRetryPolicyMaxCount.Value, UserProvidedMaxCount = true } - : new AKVRetryPolicyOptions { MaxCount = options.AzureKeyVaultRetryPolicyMaxCount.Value, UserProvidedMaxCount = true }; + : new AKVRetryPolicyOptions(maxCount: options.AzureKeyVaultRetryPolicyMaxCount.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.max-count as '{maxCount}'", options.AzureKeyVaultRetryPolicyMaxCount.Value); } @@ -2741,7 +2742,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { DelaySeconds = options.AzureKeyVaultRetryPolicyDelaySeconds.Value, UserProvidedDelaySeconds = true } - : new AKVRetryPolicyOptions { DelaySeconds = options.AzureKeyVaultRetryPolicyDelaySeconds.Value, UserProvidedDelaySeconds = true }; + : new AKVRetryPolicyOptions(delaySeconds: options.AzureKeyVaultRetryPolicyDelaySeconds.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.delay-seconds as '{delaySeconds}'", options.AzureKeyVaultRetryPolicyDelaySeconds.Value); } @@ -2756,7 +2757,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { MaxDelaySeconds = options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value, UserProvidedMaxDelaySeconds = true } - : new AKVRetryPolicyOptions { MaxDelaySeconds = options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value, UserProvidedMaxDelaySeconds = true }; + : new AKVRetryPolicyOptions(maxDelaySeconds: options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.max-delay-seconds as '{maxDelaySeconds}'", options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value); } @@ -2771,16 +2772,17 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { NetworkTimeoutSeconds = options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value, UserProvidedNetworkTimeoutSeconds = true } - : new AKVRetryPolicyOptions { NetworkTimeoutSeconds = options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value, UserProvidedNetworkTimeoutSeconds = true }; + : new AKVRetryPolicyOptions(networkTimeoutSeconds: options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.network-timeout-seconds as '{networkTimeoutSeconds}'", options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value); } - // Update Azure Key Vault options with retry policy if retry policy was modified + // Update Azure Key Vault options with retry policy if modified if (updatedRetryPolicyOptions is not null) { + // Ensure outer AKV object marks retry policy as user provided so it serializes. updatedAzureKeyVaultOptions = updatedAzureKeyVaultOptions is not null - ? updatedAzureKeyVaultOptions with { RetryPolicy = updatedRetryPolicyOptions } - : new AzureKeyVaultOptions { RetryPolicy = updatedRetryPolicyOptions }; + ? updatedAzureKeyVaultOptions with { RetryPolicy = updatedRetryPolicyOptions, UserProvidedRetryPolicy = true } + : new AzureKeyVaultOptions(retryPolicy: updatedRetryPolicyOptions); } // Update runtime config if Azure Key Vault options were modified From 12bbb9cec9c0dd3b7fe2af11658a6b26309833ab Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 6 Nov 2025 11:13:38 -0800 Subject: [PATCH 30/37] use same logic as before change --- src/Service.Tests/Configuration/ConfigurationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 05764404d8..debc43353f 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -840,7 +840,7 @@ public void MsSqlConnStringSupplementedWithAppNameProperty( bool configParsed = RuntimeConfigLoader.TryParseConfig( json: runtimeConfig.ToJson(), config: out RuntimeConfig updatedRuntimeConfig, - replacementSettings: new()); + replacementSettings: new(doReplaceEnvVar: true)); // Assert Assert.AreEqual( From 16c2f572361baaddded2e91ed83212b6feeb8dc4 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 6 Nov 2025 13:15:12 -0800 Subject: [PATCH 31/37] keep behavior the same --- src/Service.Tests/Configuration/ConfigurationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index debc43353f..fc5249449e 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -893,7 +893,7 @@ public void PgSqlConnStringSupplementedWithAppNameProperty( bool configParsed = RuntimeConfigLoader.TryParseConfig( json: runtimeConfig.ToJson(), config: out RuntimeConfig updatedRuntimeConfig, - replacementSettings: new()); + replacementSettings: new(doReplaceEnvVar: true)); // Assert Assert.AreEqual( @@ -958,7 +958,7 @@ public void TestConnectionStringIsCorrectlyUpdatedWithApplicationName( bool configParsed = RuntimeConfigLoader.TryParseConfig( json: runtimeConfig.ToJson(), config: out RuntimeConfig updatedRuntimeConfig, - replacementSettings: new()); + replacementSettings: new(doReplaceEnvVar: true)); // Assert Assert.AreEqual( From 6a223ee30c0e8cded3ad93d359e40aac91994479 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 6 Nov 2025 14:43:13 -0800 Subject: [PATCH 32/37] match enum rep logic as before --- src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs | 2 +- src/Config/DeserializationVariableReplacementSettings.cs | 4 ++-- .../UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs index 60feb7840a..4455a474e1 100644 --- a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs +++ b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs @@ -114,7 +114,7 @@ public JsonStringEnumConverterEx() public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Always replace env variable in case of Enum otherwise string to enum conversion will fail. - string? stringValue = reader.DeserializeString(new()); + string? stringValue = reader.DeserializeString(new(doReplaceEnvVar: true)); if (stringValue == null) { diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index f675656521..bffe6fbcfa 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -13,8 +13,8 @@ namespace Azure.DataApiBuilder.Config { public class DeserializationVariableReplacementSettings { - public bool DoReplaceEnvVar { get; set; } = true; - public bool DoReplaceAkvVar { get; set; } = true; + public bool DoReplaceEnvVar { get; set; } + public bool DoReplaceAkvVar { get; set; } public EnvironmentVariableReplacementFailureMode EnvFailureMode { get; set; } = EnvironmentVariableReplacementFailureMode.Throw; // @env\(' : match @env(' diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 078ac5c95c..89223d9124 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -78,13 +78,13 @@ public void CheckConfigEnvParsingTest( if (replaceEnvVar) { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)), + GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: false)), "Should read the expected config"); } else { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)), + GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: false)), "Should read the expected config"); } From 08aa404e1bd2e138e145d5bdd89ae847fd072f81 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Thu, 6 Nov 2025 15:10:33 -0800 Subject: [PATCH 33/37] fix comment to match behavior --- src/Config/FileSystemRuntimeConfigLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 4b429f301c..ce5eff3d7a 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -184,7 +184,7 @@ private void OnNewFileContentsDetected(object? sender, EventArgs e) /// The loaded RuntimeConfig, or null if none was loaded. /// ILogger for logging errors. /// When not null indicates we need to overwrite mode and how to do so. - /// Settings for variable replacement during deserialization. If null, uses default settings with environment variable replacement enabled. + /// Settings for variable replacement during deserialization. If null, uses default settings with environment variable replacement disabled. /// True if the config was loaded, otherwise false. public bool TryLoadConfig( string path, From 1e912f01cf4dfd5caf002cc61bf23fb9ce1da606 Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 7 Nov 2025 11:37:14 -0800 Subject: [PATCH 34/37] replace missing options for test and adjust for new wrapper class --- .../UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 89223d9124..bee0169a86 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -10,6 +10,7 @@ using System.Text; using System.Text.Json; using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -301,7 +302,10 @@ public void CheckConfigEnvParsingThrowExceptions(string invalidEnvVarName) { string json = @"{ ""foo"" : ""@env('envVarName'), @env('" + invalidEnvVarName + @"')"" }"; SetEnvVariables(); - Assert.ThrowsException(() => JsonSerializer.Deserialize(json)); + StringJsonConverterFactory stringConverterFactory = new(new(doReplaceEnvVar: true, envFailureMode: EnvironmentVariableReplacementFailureMode.Throw)); + JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; + options.Converters.Add(stringConverterFactory); + Assert.ThrowsException(() => JsonSerializer.Deserialize(json, options)); } [DataRow("\"notsupporteddb\"", "", From 40de92923e4b7544fd3071cd8fa016ce2f9c339c Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 7 Nov 2025 14:52:14 -0800 Subject: [PATCH 35/37] fix format for test --- src/Service.Tests/Configuration/ConfigurationTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index fc5249449e..20bc60e756 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -3319,8 +3319,7 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(SupportedHttpVerb.Post); string requestBody = @"{ ""title"": ""Harry Potter and the Order of Phoenix"", - ""publisher_id"": 1234 - }"; + ""publisher_id"": 1234"; if (includeExtraneousFieldInRequestBody) { From 42f9bddd943429912f1b476bcadb327d8c8fc45f Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 7 Nov 2025 14:58:30 -0800 Subject: [PATCH 36/37] cleanup --- src/Config/FileSystemRuntimeConfigLoader.cs | 7 ++++--- .../UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index ce5eff3d7a..dcbe13ca9a 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -304,9 +304,10 @@ public bool TryLoadConfig( public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false) { // Convert legacy replaceEnvVar parameter to replacement settings for backward compatibility - DeserializationVariableReplacementSettings? replacementSettings = replaceEnvVar - ? new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true) - : new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAkvVar: false); + DeserializationVariableReplacementSettings? replacementSettings = new ( + azureKeyVaultOptions: null, + doReplaceEnvVar: replaceEnvVar, + doReplaceAkvVar: replaceEnvVar); return TryLoadConfig(ConfigFilePath, out config, replacementSettings: replacementSettings); } diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index bee0169a86..47629ca4c8 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -90,7 +90,7 @@ public void CheckConfigEnvParsingTest( } Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)), + GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: false)), "Should read actual config"); Assert.AreEqual(expectedConfig.ToJson(), actualConfig.ToJson()); } From a351d27eb18f2b43a2a1b4ccf8e35377a31bf26e Mon Sep 17 00:00:00 2001 From: aaron burtle Date: Fri, 7 Nov 2025 15:03:44 -0800 Subject: [PATCH 37/37] format --- src/Config/FileSystemRuntimeConfigLoader.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index dcbe13ca9a..131440d752 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -304,11 +304,7 @@ public bool TryLoadConfig( public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false) { // Convert legacy replaceEnvVar parameter to replacement settings for backward compatibility - DeserializationVariableReplacementSettings? replacementSettings = new ( - azureKeyVaultOptions: null, - doReplaceEnvVar: replaceEnvVar, - doReplaceAkvVar: replaceEnvVar); - + DeserializationVariableReplacementSettings? replacementSettings = new (azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: replaceEnvVar); return TryLoadConfig(ConfigFilePath, out config, replacementSettings: replacementSettings); }