From ae71811ea4a48b5d8a53f8336390ffe5bfc13686 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:47:38 +0800 Subject: [PATCH 1/5] Merge pull request #684 from Azure/zhiyuanliang/fix-test Fix activity source test bug --- tests/Tests.AzureAppConfiguration/Unit/Tests.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Unit/Tests.cs b/tests/Tests.AzureAppConfiguration/Unit/Tests.cs index 7e68f1cf..37fb970f 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/Tests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/Tests.cs @@ -352,10 +352,12 @@ public void TestKeepSelectorPrecedenceAfterDedup() [Fact] public void TestActivitySource() { + string activitySourceName = Guid.NewGuid().ToString(); + var _activities = new List(); var _activityListener = new ActivityListener { - ShouldListenTo = source => source.Name == "Microsoft.Extensions.Configuration.AzureAppConfiguration", + ShouldListenTo = source => source.Name == activitySourceName, Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, ActivityStarted = activity => _activities.Add(activity), }; @@ -371,7 +373,11 @@ public void TestActivitySource() .ReturnsAsync(Response.FromValue(_kv, mockResponse.Object)); var config = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object)) + .AddAzureAppConfiguration(options => + { + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + options.ActivitySourceName = activitySourceName; + }) .Build(); Assert.Contains(_activities, a => a.OperationName == "Load"); From ba8846445e072906a4c700ce35dfe2aeed585ddc Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Tue, 5 Aug 2025 19:48:28 +0800 Subject: [PATCH 2/5] Fix bug with endpoint failover (#686) * update endpoint in do while * add test * in progress * update test, update logic to backoff using correct endpoint * make test more specific --------- Co-authored-by: AMER JUSUPOVIC Co-authored-by: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com> --- .../AzureAppConfigurationProvider.cs | 2 +- .../Unit/FailoverTests.cs | 84 +++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index cba9dc1b..1fe06076 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1214,7 +1214,7 @@ private async Task ExecuteWithFailOverPolicyAsync( do { - UpdateClientBackoffStatus(previousEndpoint, success); + UpdateClientBackoffStatus(_configClientManager.GetEndpointForClient(currentClient), success); clientEnumerator.MoveNext(); diff --git a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs index 810f9400..57735f37 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs @@ -415,5 +415,89 @@ ae.InnerException is AggregateException ae2 && ae2.InnerExceptions.All(ex => ex is TaskCanceledException) && ae2.InnerException is TaskCanceledException tce); } + + [Fact] + public async Task FailOverTests_AllClientsBackedOffAfterNonFailoverableException() + { + IConfigurationRefresher refresher = null; + var mockResponse = new Mock(); + + // Setup first client - succeeds on startup, fails with 404 (non-failoverable) on first refresh + var mockClient1 = new Mock(); + mockClient1.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())) + .Throws(new RequestFailedException(412, "Request failed.")) + .Throws(new RequestFailedException(412, "Request failed.")); + mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))) + .Throws(new RequestFailedException(412, "Request failed.")) + .Throws(new RequestFailedException(412, "Request failed.")); + mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new RequestFailedException(412, "Request failed.")) + .Throws(new RequestFailedException(412, "Request failed.")); + mockClient1.Setup(c => c.Equals(mockClient1)).Returns(true); + + // Setup second client - succeeds on startup, should not be called during refresh + var mockClient2 = new Mock(); + mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(Enumerable.Empty().ToList())); + mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(Response.FromValue(kv, mockResponse.Object))); + mockClient2.Setup(c => c.Equals(mockClient2)).Returns(true); + + ConfigurationClientWrapper cw1 = new ConfigurationClientWrapper(TestHelpers.PrimaryConfigStoreEndpoint, mockClient1.Object); + ConfigurationClientWrapper cw2 = new ConfigurationClientWrapper(TestHelpers.SecondaryConfigStoreEndpoint, mockClient2.Object); + + var clientList = new List() { cw1, cw2 }; + var configClientManager = new ConfigurationClientManager(clientList); + + // Verify 2 clients are available + Assert.Equal(2, configClientManager.GetClients().Count()); + + // Act & Assert - Build configuration successfully with both clients + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ClientManager = configClientManager; + options.Select("TestKey*"); + options.ConfigureRefresh(refreshOptions => + { + refreshOptions.Register("TestKey1", "label") + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + options.ReplicaDiscoveryEnabled = false; + refresher = options.GetRefresher(); + }).Build(); + + // First refresh - should call client 1 and fail with non-failoverable exception + // This should cause all clients to be backed off + await Task.Delay(1500); + await refresher.TryRefreshAsync(); + + // Verify that client 1 was called during the first refresh + mockClient1.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + + // Verify that client 2 was not called during the first refresh + mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Never); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + // Second refresh - no clients should be called as all are backed off + await Task.Delay(1500); + await refresher.TryRefreshAsync(); + + // Verify that no additional calls were made to any client during the second refresh + mockClient1.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(1)); + mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Never); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } } } From c43aab369657cbb05bbafd3d42fae8214ab1e625 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Wed, 6 Aug 2025 10:05:14 +0800 Subject: [PATCH 3/5] Support comment in json key value (#685) * support jsonc * remove unused reference * use private static option --- .../JsonKeyValueAdapter.cs | 7 +- .../Unit/JsonContentTypeTests.cs | 84 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs index 74d2b882..d353439f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/JsonKeyValueAdapter.cs @@ -15,6 +15,11 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration { internal class JsonKeyValueAdapter : IKeyValueAdapter { + private static readonly JsonDocumentOptions JsonParseOptions = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip + }; + public Task>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken) { if (setting == null) @@ -28,7 +33,7 @@ public Task>> ProcessKeyValue(Configura try { - using (JsonDocument document = JsonDocument.Parse(rootJson)) + using (JsonDocument document = JsonDocument.Parse(rootJson, JsonParseOptions)) { keyValuePairs = new JsonFlattener().FlattenJson(document.RootElement); } diff --git a/tests/Tests.AzureAppConfiguration/Unit/JsonContentTypeTests.cs b/tests/Tests.AzureAppConfiguration/Unit/JsonContentTypeTests.cs index 91cb03a8..eaca360f 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/JsonContentTypeTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/JsonContentTypeTests.cs @@ -276,6 +276,90 @@ public void JsonContentTypeTests_JsonKeyValueAdapterCannotProcessKeyVaultReferen Assert.False(jsonKeyValueAdapter.CanProcess(setting)); } + [Fact] + public void JsonContentTypeTests_LoadJsonValuesWithComments() + { + List _kvCollection = new List + { + // Test various comment styles and positions + ConfigurationModelFactory.ConfigurationSetting( + key: "MixedCommentStyles", + value: @"{ + // Single line comment at start + ""ApiSettings"": { + ""BaseUrl"": ""https://api.example.com"", // Inline single line + /* Multi-line comment + spanning multiple lines */ + ""ApiKey"": ""secret-key"", + ""Endpoints"": [ + // Comment before array element + ""/users"", + /* Comment between elements */ + ""/orders"", + ""/products"" // Comment after element + ] + }, + // Test edge cases + ""StringWithSlashes"": ""This is not a // comment"", + ""StringWithStars"": ""This is not a /* comment */"", + ""UrlValue"": ""https://example.com/path"", // This is a real comment + ""EmptyComment"": ""value"", // + /**/ + ""AfterEmptyComment"": ""value2"" + /* Final multi-line comment */ + }", + contentType: "application/json"), + // Test invalid JSON with comments + ConfigurationModelFactory.ConfigurationSetting( + key: "InvalidJsonWithComments", + value: @"// This is a comment + { invalid json structure + // Another comment + missing quotes and braces", + contentType: "application/json"), + // Test only comments (should be invalid JSON) + ConfigurationModelFactory.ConfigurationSetting( + key: "OnlyComments", + value: @" + // Just comments + /* No actual content */ + ", + contentType: "application/json") + }; + + var mockClientManager = GetMockConfigurationClientManager(_kvCollection); + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => options.ClientManager = mockClientManager) + .Build(); + + // Verify mixed comment styles are properly parsed + Assert.Equal("https://api.example.com", config["MixedCommentStyles:ApiSettings:BaseUrl"]); + Assert.Equal("secret-key", config["MixedCommentStyles:ApiSettings:ApiKey"]); + Assert.Equal("/users", config["MixedCommentStyles:ApiSettings:Endpoints:0"]); + Assert.Equal("/orders", config["MixedCommentStyles:ApiSettings:Endpoints:1"]); + Assert.Equal("/products", config["MixedCommentStyles:ApiSettings:Endpoints:2"]); + + // Verify edge cases where comment-like text appears in strings + Assert.Equal("This is not a // comment", config["MixedCommentStyles:StringWithSlashes"]); + Assert.Equal("This is not a /* comment */", config["MixedCommentStyles:StringWithStars"]); + Assert.Equal("https://example.com/path", config["MixedCommentStyles:UrlValue"]); + Assert.Equal("value", config["MixedCommentStyles:EmptyComment"]); + Assert.Equal("value2", config["MixedCommentStyles:AfterEmptyComment"]); + + // Invalid JSON should fall back to string value + Assert.Equal(@"// This is a comment + { invalid json structure + // Another comment + missing quotes and braces", config["InvalidJsonWithComments"]); + + // Only comments should be treated as string value (invalid JSON) + Assert.Equal(@" + // Just comments + /* No actual content */ + ", config["OnlyComments"]); + } + private IConfigurationClientManager GetMockConfigurationClientManager(List _kvCollection) { var mockResponse = new Mock(); From 17f073235d370beb1cbe48b820365ecf63941ac9 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 21 Aug 2025 10:38:52 +0800 Subject: [PATCH 4/5] Add request tracing for Aspire usage (#687) * add request tracing for aspire component * update tag name --- .../AzureAppConfigurationProvider.cs | 2 ++ .../Constants/RequestTracingConstants.cs | 2 ++ .../RequestTracingOptions.cs | 5 +++++ .../TracingUtils.cs | 5 +++++ 4 files changed, 14 insertions(+) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 1fe06076..27cd262e 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1331,6 +1331,8 @@ private void EnsureAssemblyInspected() _requestTracingOptions.FeatureManagementAspNetCoreVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.FeatureManagementAspNetCoreAssemblyName); + _requestTracingOptions.AspireComponentVersion = TracingUtils.GetAssemblyVersion(RequestTracingConstants.AspireComponentAssemblyName); + if (TracingUtils.GetAssemblyVersion(RequestTracingConstants.SignalRAssemblyName) != null) { _requestTracingOptions.IsSignalRUsed = true; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs index 979ea9ad..340fba85 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs @@ -24,6 +24,7 @@ internal class RequestTracingConstants public const string EnvironmentKey = "Env"; public const string FeatureManagementVersionKey = "FMVer"; public const string FeatureManagementAspNetCoreVersionKey = "FMANCVer"; + public const string AspireComponentVersionKey = "DNACVer"; public const string DevEnvironmentValue = "Dev"; public const string KeyVaultConfiguredTag = "UsesKeyVault"; public const string KeyVaultRefreshConfiguredTag = "RefreshesKeyVault"; @@ -53,6 +54,7 @@ internal class RequestTracingConstants public const string FeatureManagementAssemblyName = "Microsoft.FeatureManagement"; public const string FeatureManagementAspNetCoreAssemblyName = "Microsoft.FeatureManagement.AspNetCore"; + public const string AspireComponentAssemblyName = "Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration"; public const string SignalRAssemblyName = "Microsoft.AspNetCore.SignalR"; public const string Delimiter = "+"; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs index 21582db1..8a3a7b20 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs @@ -50,6 +50,11 @@ internal class RequestTracingOptions /// public string FeatureManagementAspNetCoreVersion { get; set; } + /// + /// Version of the Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration assembly, if present in the application. + /// + public string AspireComponentVersion { get; set; } + /// /// Flag to indicate whether Microsoft.AspNetCore.SignalR assembly is present in the application. /// diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs index b3e12913..33050b5b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs @@ -181,6 +181,11 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeatureManagementAspNetCoreVersionKey, requestTracingOptions.FeatureManagementAspNetCoreVersion)); } + if (requestTracingOptions.AspireComponentVersion != null) + { + correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.AspireComponentVersionKey, requestTracingOptions.AspireComponentVersion)); + } + if (requestTracingOptions.UsesAnyTracingFeature()) { correlationContextKeyValues.Add(new KeyValuePair(RequestTracingConstants.FeaturesKey, requestTracingOptions.CreateFeaturesString())); From c17524a11efa30b341a68c57a37291fc7f847605 Mon Sep 17 00:00:00 2001 From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:01:26 +0800 Subject: [PATCH 5/5] version bump 8.4.0 (#690) --- .../Microsoft.Azure.AppConfiguration.AspNetCore.csproj | 2 +- .../Microsoft.Azure.AppConfiguration.Functions.Worker.csproj | 2 +- ...rosoft.Extensions.Configuration.AzureAppConfiguration.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj index 2477d611..cc55dcf3 100644 --- a/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj +++ b/src/Microsoft.Azure.AppConfiguration.AspNetCore/Microsoft.Azure.AppConfiguration.AspNetCore.csproj @@ -21,7 +21,7 @@ - 8.3.0 + 8.4.0 diff --git a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj index 1990839d..2a359874 100644 --- a/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj +++ b/src/Microsoft.Azure.AppConfiguration.Functions.Worker/Microsoft.Azure.AppConfiguration.Functions.Worker.csproj @@ -24,7 +24,7 @@ - 8.3.0 + 8.4.0 diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj index 4a1ce37a..022a2084 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj @@ -38,7 +38,7 @@ - 8.3.0 + 8.4.0