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/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index cba9dc1b..27cd262e 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();
@@ -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/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/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
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()));
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);
+ }
}
}
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();
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");