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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<!-- Nuget Package Version Settings -->

<PropertyGroup>
<OfficialVersion>8.3.0</OfficialVersion>
<OfficialVersion>8.4.0</OfficialVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(CDP_PATCH_NUMBER)'!='' AND '$(CDP_BUILD_TYPE)'=='Official'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<!-- Nuget Package Version Settings -->

<PropertyGroup>
<OfficialVersion>8.3.0</OfficialVersion>
<OfficialVersion>8.4.0</OfficialVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(CDP_PATCH_NUMBER)'!='' AND '$(CDP_BUILD_TYPE)'=='Official'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,7 @@ private async Task<T> ExecuteWithFailOverPolicyAsync<T>(

do
{
UpdateClientBackoffStatus(previousEndpoint, success);
UpdateClientBackoffStatus(_configClientManager.GetEndpointForClient(currentClient), success);

clientEnumerator.MoveNext();

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = "+";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IEnumerable<KeyValuePair<string, string>>> ProcessKeyValue(ConfigurationSetting setting, Uri endpoint, Logger logger, CancellationToken cancellationToken)
{
if (setting == null)
Expand All @@ -28,7 +33,7 @@ public Task<IEnumerable<KeyValuePair<string, string>>> ProcessKeyValue(Configura

try
{
using (JsonDocument document = JsonDocument.Parse(rootJson))
using (JsonDocument document = JsonDocument.Parse(rootJson, JsonParseOptions))
{
keyValuePairs = new JsonFlattener().FlattenJson(document.RootElement);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<!-- Nuget Package Version Settings -->

<PropertyGroup>
<OfficialVersion>8.3.0</OfficialVersion>
<OfficialVersion>8.4.0</OfficialVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(CDP_PATCH_NUMBER)'!='' AND '$(CDP_BUILD_TYPE)'=='Official'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ internal class RequestTracingOptions
/// </summary>
public string FeatureManagementAspNetCoreVersion { get; set; }

/// <summary>
/// Version of the Aspire.Microsoft.Extensions.Configuration.AzureAppConfiguration assembly, if present in the application.
/// </summary>
public string AspireComponentVersion { get; set; }

/// <summary>
/// Flag to indicate whether Microsoft.AspNetCore.SignalR assembly is present in the application.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re
correlationContextKeyValues.Add(new KeyValuePair<string, string>(RequestTracingConstants.FeatureManagementAspNetCoreVersionKey, requestTracingOptions.FeatureManagementAspNetCoreVersion));
}

if (requestTracingOptions.AspireComponentVersion != null)
{
correlationContextKeyValues.Add(new KeyValuePair<string, string>(RequestTracingConstants.AspireComponentVersionKey, requestTracingOptions.AspireComponentVersion));
}

if (requestTracingOptions.UsesAnyTracingFeature())
{
correlationContextKeyValues.Add(new KeyValuePair<string, string>(RequestTracingConstants.FeaturesKey, requestTracingOptions.CreateFeaturesString()));
Expand Down
84 changes: 84 additions & 0 deletions tests/Tests.AzureAppConfiguration/Unit/FailoverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response>();

// Setup first client - succeeds on startup, fails with 404 (non-failoverable) on first refresh
var mockClient1 = new Mock<ConfigurationClient>();
mockClient1.SetupSequence(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()))
.Returns(new MockAsyncPageable(Enumerable.Empty<ConfigurationSetting>().ToList()))
.Throws(new RequestFailedException(412, "Request failed."))
.Throws(new RequestFailedException(412, "Request failed."));
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(kv, mockResponse.Object)))
.Throws(new RequestFailedException(412, "Request failed."))
.Throws(new RequestFailedException(412, "Request failed."));
mockClient1.SetupSequence(c => c.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.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<ConfigurationClient>();
mockClient2.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()))
.Returns(new MockAsyncPageable(Enumerable.Empty<ConfigurationSetting>().ToList()));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(kv, mockResponse.Object)));
mockClient2.Setup(c => c.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()))
.Returns(Task.FromResult(Response.FromValue<ConfigurationSetting>(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<ConfigurationClientWrapper>() { 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<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(1));

// Verify that client 2 was not called during the first refresh
mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Never);
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), 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<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
mockClient1.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Exactly(1));
mockClient2.Verify(mc => mc.GetConfigurationSettingsAsync(It.IsAny<SettingSelector>(), It.IsAny<CancellationToken>()), Times.Never);
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
mockClient2.Verify(mc => mc.GetConfigurationSettingAsync(It.IsAny<ConfigurationSetting>(), It.IsAny<bool>(), It.IsAny<CancellationToken>()), Times.Never);
}
}
}
84 changes: 84 additions & 0 deletions tests/Tests.AzureAppConfiguration/Unit/JsonContentTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,90 @@ public void JsonContentTypeTests_JsonKeyValueAdapterCannotProcessKeyVaultReferen
Assert.False(jsonKeyValueAdapter.CanProcess(setting));
}

[Fact]
public void JsonContentTypeTests_LoadJsonValuesWithComments()
{
List<ConfigurationSetting> _kvCollection = new List<ConfigurationSetting>
{
// 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<ConfigurationSetting> _kvCollection)
{
var mockResponse = new Mock<Response>();
Expand Down
10 changes: 8 additions & 2 deletions tests/Tests.AzureAppConfiguration/Unit/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,12 @@ public void TestKeepSelectorPrecedenceAfterDedup()
[Fact]
public void TestActivitySource()
{
string activitySourceName = Guid.NewGuid().ToString();

var _activities = new List<Activity>();
var _activityListener = new ActivityListener
{
ShouldListenTo = source => source.Name == "Microsoft.Extensions.Configuration.AzureAppConfiguration",
ShouldListenTo = source => source.Name == activitySourceName,
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
ActivityStarted = activity => _activities.Add(activity),
};
Expand All @@ -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");
Expand Down
Loading