From 5910dbf11d41445b75060b30d78adb1342b092f2 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Mon, 21 Apr 2025 09:26:11 -0700
Subject: [PATCH 01/12] update package versions to 8.1.2 (#648)
---
.../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 4cd6bf4e7..b9f6bfc3f 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.1.1
+ 8.1.2
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 e327421b7..6236ba4f8 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.1.1
+ 8.1.2
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 91b90bb1a..d5af2b14b 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Microsoft.Extensions.Configuration.AzureAppConfiguration.csproj
@@ -35,7 +35,7 @@
- 8.1.1
+ 8.1.2
From 65ed48033482e796143b41fa150d06293fbf8316 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Mon, 21 Apr 2025 13:12:54 -0700
Subject: [PATCH 02/12] Revert "Shorten the defeult timeout of individual call
to backend (#620)" (#653)
This reverts commit 87f0f85ca2e4011f82d93a864e35c6c804cd6c39.
---
.../AzureAppConfigurationOptions.cs | 7 -------
.../AzureAppConfigurationProvider.cs | 7 -------
2 files changed, 14 deletions(-)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
index 6be96b65c..6e600fa2a 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license.
//
using Azure.Core;
-using Azure.Core.Pipeline;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
@@ -11,7 +10,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net.Http;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
@@ -24,7 +22,6 @@ public class AzureAppConfigurationOptions
{
private const int MaxRetries = 2;
private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromMinutes(1);
- private static readonly TimeSpan NetworkTimeout = TimeSpan.FromSeconds(10);
private static readonly KeyValueSelector DefaultQuery = new KeyValueSelector { KeyFilter = KeyFilter.Any, LabelFilter = LabelFilter.Null };
private List _individualKvWatchers = new List();
@@ -513,10 +510,6 @@ private static ConfigurationClientOptions GetDefaultClientOptions()
clientOptions.Retry.MaxDelay = MaxRetryDelay;
clientOptions.Retry.Mode = RetryMode.Exponential;
clientOptions.AddPolicy(new UserAgentHeaderPolicy(), HttpPipelinePosition.PerCall);
- clientOptions.Transport = new HttpClientTransport(new HttpClient()
- {
- Timeout = NetworkTimeout
- });
return clientOptions;
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index d86133ae2..5e1bf8e07 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -1221,13 +1221,6 @@ await ExecuteWithFailOverPolicyAsync
-
+
+
+
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueSelector.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueSelector.cs
index 54bda1a49..f01eb6559 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueSelector.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueSelector.cs
@@ -1,6 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Models
{
///
@@ -24,6 +28,11 @@ public class KeyValueSelector
///
public string SnapshotName { get; set; }
+ ///
+ /// A filter that determines what tags to require when selecting key-values for the the configuration provider.
+ ///
+ public IEnumerable TagFilters { get; set; }
+
///
/// A boolean that signifies whether this selector is intended to select feature flags.
///
@@ -40,7 +49,11 @@ public override bool Equals(object obj)
{
return KeyFilter == selector.KeyFilter
&& LabelFilter == selector.LabelFilter
- && SnapshotName == selector.SnapshotName;
+ && SnapshotName == selector.SnapshotName
+ && (TagFilters == null
+ ? selector.TagFilters == null
+ : selector.TagFilters != null && new HashSet(TagFilters).SetEquals(selector.TagFilters))
+ && IsFeatureFlagSelector == selector.IsFeatureFlagSelector;
}
return false;
@@ -52,9 +65,22 @@ public override bool Equals(object obj)
/// A hash code for the current object.
public override int GetHashCode()
{
- return (KeyFilter?.GetHashCode() ?? 0) ^
- (LabelFilter?.GetHashCode() ?? 1) ^
- (SnapshotName?.GetHashCode() ?? 2);
+ string tagFiltersString = string.Empty;
+
+ if (TagFilters != null && TagFilters.Any())
+ {
+ var sortedTags = new SortedSet(TagFilters);
+
+ // Concatenate tags into a single string with a delimiter
+ tagFiltersString = string.Join("\n", sortedTags);
+ }
+
+ return HashCode.Combine(
+ KeyFilter,
+ LabelFilter,
+ SnapshotName,
+ tagFiltersString,
+ IsFeatureFlagSelector);
}
}
}
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs
index 616f8bcd1..a9f59e745 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Models/KeyValueWatcher.cs
@@ -3,6 +3,7 @@
//
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using System;
+using System.Collections.Generic;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Models
{
@@ -18,6 +19,11 @@ internal class KeyValueWatcher
///
public string Label { get; set; }
+ ///
+ /// Tags of the key-value to be watched.
+ ///
+ public IEnumerable Tags { get; set; }
+
///
/// A flag to refresh all key-values.
///
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TagValue.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TagValue.cs
new file mode 100644
index 000000000..7522e7e13
--- /dev/null
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TagValue.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
+{
+ ///
+ /// Defines well known tag values that are used within Azure App Configuration.
+ ///
+ public class TagValue
+ {
+ ///
+ /// Matches null tag values.
+ ///
+ public const string Null = "\0";
+ }
+}
diff --git a/tests/Tests.AzureAppConfiguration/TagFiltersTests.cs b/tests/Tests.AzureAppConfiguration/TagFiltersTests.cs
new file mode 100644
index 000000000..f0e4e2636
--- /dev/null
+++ b/tests/Tests.AzureAppConfiguration/TagFiltersTests.cs
@@ -0,0 +1,629 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Azure;
+using Azure.Data.AppConfiguration;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration;
+using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Tests.AzureAppConfiguration
+{
+ public class TagFiltersTests
+ {
+ private List _kvCollection;
+ private const int MaxTagFilters = 5;
+
+ public TagFiltersTests()
+ {
+ _kvCollection = new List
+ {
+ CreateConfigurationSetting("TestKey1", "label", "TestValue1", "0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63",
+ new Dictionary {
+ { "Environment", "Development" },
+ { "App", "TestApp" }
+ }),
+
+ CreateConfigurationSetting("TestKey2", "label", "TestValue2", "31c38369-831f-4bf1-b9ad-79db56c8b989",
+ new Dictionary {
+ { "Environment", "Production" },
+ { "App", "TestApp" }
+ }),
+
+ CreateConfigurationSetting("TestKey3", "label", "TestValue3", "bb203f2b-c113-44fc-995d-b933c2143339",
+ new Dictionary {
+ { "Environment", "Development" },
+ { "Component", "API" }
+ }),
+
+ CreateConfigurationSetting("TestKey4", "label", "TestValue4", "bb203f2b-c113-44fc-995d-b933c2143340",
+ new Dictionary {
+ { "Environment", "Staging" },
+ { "App", "TestApp" },
+ { "Component", "Frontend" }
+ }),
+
+ CreateConfigurationSetting("TestKey5", "label", "TestValue5", "bb203f2b-c113-44fc-995d-b933c2143341",
+ new Dictionary {
+ { "Special:Tag", "Value:With:Colons" },
+ { "Tag@With@At", "Value@With@At" }
+ }),
+
+ CreateConfigurationSetting("TestKey6", "label", "TestValue6", "bb203f2b-c113-44fc-995d-b933c2143342",
+ new Dictionary {
+ { "Tag,With,Commas", "Value,With,Commas" },
+ { "Simple", "Tag" },
+ { "EmptyTag", "" },
+ { "NullTag", null }
+ }),
+
+ CreateFeatureFlagSetting("Feature1", "label", true, "0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63",
+ new Dictionary {
+ { "Environment", "Development" },
+ { "App", "TestApp" }
+ }),
+
+ CreateFeatureFlagSetting("Feature2", "label", false, "31c38369-831f-4bf1-b9ad-79db56c8b989",
+ new Dictionary {
+ { "Environment", "Production" },
+ { "App", "TestApp" }
+ }),
+
+ CreateFeatureFlagSetting("Feature3", "label", true, "bb203f2b-c113-44fc-995d-b933c2143339",
+ new Dictionary {
+ { "Environment", "Development" },
+ { "Component", "API" }
+ }),
+
+ CreateFeatureFlagSetting("Feature4", "label", false, "bb203f2b-c113-44fc-995d-b933c2143340",
+ new Dictionary {
+ { "Environment", "Staging" },
+ { "App", "TestApp" },
+ { "Component", "Frontend" }
+ }),
+
+ CreateFeatureFlagSetting("Feature5", "label", true, "bb203f2b-c113-44fc-995d-b933c2143341",
+ new Dictionary {
+ { "Special:Tag", "Value:With:Colons" },
+ { "Tag@With@At", "Value@With@At" }
+ }),
+
+ CreateFeatureFlagSetting("Feature6", "label", false, "bb203f2b-c113-44fc-995d-b933c2143342",
+ new Dictionary {
+ { "Tag,With,Commas", "Value,With,Commas" },
+ { "Simple", "Tag" },
+ { "EmptyTag", "" },
+ { "NullTag", null }
+ }),
+ };
+ }
+
+ private ConfigurationSetting CreateConfigurationSetting(string key, string label, string value, string etag, IDictionary tags)
+ {
+ // Create the setting without tags
+ var setting = ConfigurationModelFactory.ConfigurationSetting(
+ key: key,
+ label: label,
+ value: value,
+ eTag: new ETag(etag),
+ contentType: "text");
+
+ // Add tags to the setting
+ if (tags != null)
+ {
+ foreach (var tag in tags)
+ {
+ setting.Tags.Add(tag.Key, tag.Value);
+ }
+ }
+
+ return setting;
+ }
+
+ private ConfigurationSetting CreateFeatureFlagSetting(string featureId, string label, bool enabled, string etag, IDictionary tags)
+ {
+ string jsonValue = $@"
+ {{
+ ""id"": ""{featureId}"",
+ ""description"": ""Test feature flag"",
+ ""enabled"": {enabled.ToString().ToLowerInvariant()},
+ ""conditions"": {{
+ ""client_filters"": []
+ }}
+ }}";
+
+ // Create the feature flag setting
+ var setting = ConfigurationModelFactory.ConfigurationSetting(
+ key: FeatureManagementConstants.FeatureFlagMarker + featureId,
+ label: label,
+ value: jsonValue,
+ eTag: new ETag(etag),
+ contentType: FeatureManagementConstants.ContentType + ";charset=utf-8");
+
+ // Add tags to the setting
+ if (tags != null)
+ {
+ foreach (var tag in tags)
+ {
+ setting.Tags.Add(tag.Key, tag.Value);
+ }
+ }
+
+ return setting;
+ }
+
+ [Fact]
+ public void TagFiltersTests_BasicTagFiltering()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Contains("Environment=Development")),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection.FindAll(kv =>
+ kv.Tags.ContainsKey("Environment") && kv.Tags["Environment"] == "Development")));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { "Environment=Development" });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List { "Environment=Development" });
+ });
+ })
+ .Build();
+
+ // Only TestKey1 and TestKey3 have Environment=Development tag
+ Assert.Equal("TestValue1", config["TestKey1"]);
+ Assert.Equal("TestValue3", config["TestKey3"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+ Assert.Null(config["TestKey6"]);
+
+ Assert.NotNull(config["FeatureManagement:Feature1"]);
+ Assert.NotNull(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ Assert.Null(config["FeatureManagement:Feature6"]);
+ }
+
+ [Fact]
+ public void TagFiltersTests_NullOrEmptyValue()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Contains("EmptyTag=") &&
+ s.TagsFilter.Contains($"NullTag={TagValue.Null}")),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection.FindAll(kv =>
+ kv.Tags.ContainsKey("EmptyTag") && kv.Tags["EmptyTag"] == "" &&
+ kv.Tags.ContainsKey("NullTag") && kv.Tags["NullTag"] == null)));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { "EmptyTag=", $"NullTag={TagValue.Null}" });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List { "EmptyTag=", $"NullTag={TagValue.Null}" });
+ });
+ })
+ .Build();
+
+ // Only TestKey6 and Feature6 have EmptyTag and NullTag
+ Assert.Null(config["TestKey1"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey3"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+ Assert.Equal("TestValue6", config["TestKey6"]);
+
+ Assert.Null(config["FeatureManagement:Feature1"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ Assert.NotNull(config["FeatureManagement:Feature6"]);
+ }
+
+ [Fact]
+ public void TagFiltersTests_MultipleTagsFiltering()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Contains("App=TestApp") &&
+ s.TagsFilter.Contains("Environment=Development")),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection.FindAll(kv =>
+ kv.Tags.ContainsKey("App") && kv.Tags["App"] == "TestApp" &&
+ kv.Tags.ContainsKey("Environment") && kv.Tags["Environment"] == "Development")));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { "App=TestApp", "Environment=Development" });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List { "App=TestApp", "Environment=Development" });
+ });
+ })
+ .Build();
+
+ Assert.Equal("TestValue1", config["TestKey1"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey3"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+ Assert.Null(config["TestKey6"]);
+
+ Assert.NotNull(config["FeatureManagement:Feature1"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ Assert.Null(config["FeatureManagement:Feature6"]);
+ }
+
+ [Fact]
+ public void TagFiltersTests_InvalidTagFormat()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ List invalidTagFilters = new List { "InvalidTagFormat", "=tagValue", "", null };
+
+ foreach (string tagsFilter in invalidTagFilters)
+ {
+ // Verify that an ArgumentException is thrown when using an invalid tag format
+ var exception = Assert.Throws(() =>
+ {
+ new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { tagsFilter });
+ })
+ .Build();
+ });
+
+ Assert.Contains($"Tag filter '{tagsFilter}' does not follow the format \"tagName=tagValue\".", exception.Message);
+ }
+ }
+
+ [Fact]
+ public void TagFiltersTests_TooManyTags()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+ var mockResponse = new Mock();
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Contains("Environment=Development") && s.TagsFilter.Count <= MaxTagFilters),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection.FindAll(kv =>
+ kv.Tags.ContainsKey("Environment") && kv.Tags["Environment"] == "Development")));
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Count > MaxTagFilters),
+ It.IsAny()))
+ .Throws(new RequestFailedException($"Invalid parameter TagsFilter. Maximum filters is {MaxTagFilters}"));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { "Environment=Development" });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List { "Environment=Development" });
+ });
+ })
+ .Build();
+
+ List longTagsFilter = new List
+ {
+ "Environment=Development",
+ "Environment=Development",
+ "Environment=Development",
+ "Environment=Development",
+ "Environment=Development",
+ "Environment=Development"
+ };
+
+ // Verify that a RequestFailedException is thrown when passing more than the allowed number of tags
+ var exception = Assert.Throws(() =>
+ {
+ new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", longTagsFilter);
+ })
+ .Build();
+ });
+ }
+
+ [Fact]
+ public void TagFiltersTests_TagFilterInteractionWithKeyLabelFilters()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ // Setup mock to verify that all three filters (key, label, tags) are correctly applied together
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ (s.KeyFilter == "TestKey*" || s.KeyFilter == FeatureManagementConstants.FeatureFlagMarker + "Feature1") &&
+ s.LabelFilter == "label" &&
+ s.TagsFilter.Contains("Environment=Development")),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection.FindAll(kv =>
+ (kv.Key.StartsWith("TestKey") || kv.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker + "Feature1")) &&
+ kv.Label == "label" &&
+ kv.Tags.ContainsKey("Environment") &&
+ kv.Tags["Environment"] == "Development")));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select("TestKey*", "label", new List { "Environment=Development" });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select("Feature1", "label", new List { "Environment=Development" });
+ });
+ })
+ .Build();
+
+ // Only TestKey1 and TestKey3 match all criteria
+ Assert.Equal("TestValue1", config["TestKey1"]);
+ Assert.Equal("TestValue3", config["TestKey3"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+ Assert.Null(config["TestKey6"]);
+
+ Assert.NotNull(config["FeatureManagement:Feature1"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ Assert.Null(config["FeatureManagement:Feature6"]);
+ }
+
+ [Fact]
+ public void TagFiltersTests_EmptyTagsCollection()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ // Setup mock to verify behavior with empty tags collection
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Count == 0),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List());
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List());
+ });
+ })
+ .Build();
+
+ // All keys should be returned when no tag filtering is applied
+ Assert.Equal("TestValue1", config["TestKey1"]);
+ Assert.Equal("TestValue2", config["TestKey2"]);
+ Assert.Equal("TestValue3", config["TestKey3"]);
+ Assert.Equal("TestValue4", config["TestKey4"]);
+ Assert.Equal("TestValue5", config["TestKey5"]);
+ Assert.Equal("TestValue6", config["TestKey6"]);
+
+ Assert.NotNull(config["FeatureManagement:Feature1"]);
+ Assert.NotNull(config["FeatureManagement:Feature2"]);
+ Assert.NotNull(config["FeatureManagement:Feature3"]);
+ Assert.NotNull(config["FeatureManagement:Feature4"]);
+ Assert.NotNull(config["FeatureManagement:Feature5"]);
+ Assert.NotNull(config["FeatureManagement:Feature6"]);
+ }
+
+ [Fact]
+ public void TagFiltersTests_SpecialCharactersInTags()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ // Setup mock for special characters in tags
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Contains("Special:Tag=Value:With:Colons")),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection.FindAll(kv =>
+ kv.Tags.ContainsKey("Special:Tag") && kv.Tags["Special:Tag"] == "Value:With:Colons")));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { "Special:Tag=Value:With:Colons" });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List { "Special:Tag=Value:With:Colons" });
+ });
+ })
+ .Build();
+
+ // Only TestKey5 has the special character tag
+ Assert.Equal("TestValue5", config["TestKey5"]);
+ Assert.Null(config["TestKey1"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey3"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey6"]);
+
+ Assert.NotNull(config["FeatureManagement:Feature5"]);
+ Assert.Null(config["FeatureManagement:Feature1"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature6"]);
+ }
+
+ [Fact]
+ public void TagFiltersTests_EscapedCommaCharactersInTags()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+
+ // Setup mock for comma characters in tags that need to be escaped with backslash
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.Is(s =>
+ s.TagsFilter.Contains(@"Tag\,With\,Commas=Value\,With\,Commas")),
+ It.IsAny()))
+ .Returns(new MockAsyncPageable(_kvCollection.FindAll(kv =>
+ kv.Tags.ContainsKey("Tag,With,Commas") && kv.Tags["Tag,With,Commas"] == "Value,With,Commas")));
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { @"Tag\,With\,Commas=Value\,With\,Commas" });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List { @"Tag\,With\,Commas=Value\,With\,Commas" });
+ });
+ })
+ .Build();
+
+ // Only TestKey6 has the tag with commas
+ Assert.Equal("TestValue6", config["TestKey6"]);
+ Assert.Null(config["TestKey1"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey3"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+
+ Assert.NotNull(config["FeatureManagement:Feature6"]);
+ Assert.Null(config["FeatureManagement:Feature1"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ }
+
+ [Fact]
+ public async Task TagFiltersTests_BasicRefresh()
+ {
+ var mockClient = new Mock(MockBehavior.Strict);
+ IConfigurationRefresher refresher = null;
+
+ var mockAsyncPageable = new MockAsyncPageable(_kvCollection);
+
+ mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()))
+ .Callback(() => mockAsyncPageable.UpdateCollection(_kvCollection.FindAll(kv =>
+ kv.Tags.ContainsKey("Environment") && kv.Tags["Environment"] == "Development")))
+ .Returns(mockAsyncPageable);
+
+ var config = new ConfigurationBuilder()
+ .AddAzureAppConfiguration(options =>
+ {
+ options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object);
+ options.Select(KeyFilter.Any, "label", new List { "Environment=Development" });
+ options.ConfigurationSettingPageIterator = new MockConfigurationSettingPageIterator();
+ options.ConfigureRefresh(refreshOptions =>
+ {
+ refreshOptions.RegisterAll();
+ refreshOptions.SetRefreshInterval(TimeSpan.FromSeconds(1));
+ });
+ options.UseFeatureFlags(ff =>
+ {
+ ff.Select(KeyFilter.Any, "label", new List { "Environment=Development" });
+ ff.SetRefreshInterval(TimeSpan.FromSeconds(1));
+ });
+ refresher = options.GetRefresher();
+ })
+ .Build();
+
+ // Only TestKey1 and TestKey3 have Environment=Development tag
+ Assert.Equal("TestValue1", config["TestKey1"]);
+ Assert.Equal("TestValue3", config["TestKey3"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+ Assert.Null(config["TestKey6"]);
+
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
+ Assert.NotNull(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ Assert.Null(config["FeatureManagement:Feature6"]);
+
+ _kvCollection.Find(setting => setting.Key == "TestKey1").Value = "UpdatedValue1";
+
+ _kvCollection.Find(setting => setting.Key == FeatureManagementConstants.FeatureFlagMarker + "Feature1").Value = $@"
+ {{
+ ""id"": ""Feature1"",
+ ""description"": ""Test feature flag"",
+ ""enabled"": false,
+ ""conditions"": {{
+ ""client_filters"": []
+ }}
+ }}";
+
+ await Task.Delay(1500);
+
+ await refresher.RefreshAsync();
+
+ Assert.Equal("UpdatedValue1", config["TestKey1"]);
+ Assert.Equal("TestValue3", config["TestKey3"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+ Assert.Null(config["TestKey6"]);
+
+ Assert.Equal("False", config["FeatureManagement:Feature1"]);
+ Assert.NotNull(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ Assert.Null(config["FeatureManagement:Feature6"]);
+
+ _kvCollection.Find(setting => setting.Key == FeatureManagementConstants.FeatureFlagMarker + "Feature1").Value = $@"
+ {{
+ ""id"": ""Feature1"",
+ ""description"": ""Test feature flag"",
+ ""enabled"": true,
+ ""conditions"": {{
+ ""client_filters"": []
+ }}
+ }}";
+
+ await Task.Delay(1500);
+
+ await refresher.RefreshAsync();
+
+ Assert.Equal("UpdatedValue1", config["TestKey1"]);
+ Assert.Equal("TestValue3", config["TestKey3"]);
+ Assert.Null(config["TestKey2"]);
+ Assert.Null(config["TestKey4"]);
+ Assert.Null(config["TestKey5"]);
+ Assert.Null(config["TestKey6"]);
+
+ Assert.Equal("True", config["FeatureManagement:Feature1"]);
+ Assert.NotNull(config["FeatureManagement:Feature3"]);
+ Assert.Null(config["FeatureManagement:Feature2"]);
+ Assert.Null(config["FeatureManagement:Feature4"]);
+ Assert.Null(config["FeatureManagement:Feature5"]);
+ Assert.Null(config["FeatureManagement:Feature6"]);
+ }
+ }
+}
From 22af6e2ed71eaeadf20c4e35b29896834769c464 Mon Sep 17 00:00:00 2001
From: Amer Jusupovic <32405726+amerjusupovic@users.noreply.github.com>
Date: Fri, 9 May 2025 14:18:29 -0700
Subject: [PATCH 05/12] Shorten default timeout of individual calls to backend
(#657)
* in progress fix shorten timeout PR
* dispose httpclienttransport
* remove unnecessary check
* fix disposal pattern
* fix static compile error
* remove unused using
* reset options
* fix options
* add line to options
* use retryoptions.networktimeout
* add test, update isfailoverable
* update test comment
* update test
* remove check for nested taskcanceledexception
* simplify if statement in isfailoverable
---
.../AzureAppConfigurationOptions.cs | 2 +
.../AzureAppConfigurationProvider.cs | 5 ++
.../FailoverTests.cs | 79 +++++++++++++++++++
3 files changed, 86 insertions(+)
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
index 975f1ab32..9b33c1334 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs
@@ -22,6 +22,7 @@ public class AzureAppConfigurationOptions
{
private const int MaxRetries = 2;
private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromMinutes(1);
+ private static readonly TimeSpan NetworkTimeout = TimeSpan.FromSeconds(10);
private static readonly KeyValueSelector DefaultQuery = new KeyValueSelector { KeyFilter = KeyFilter.Any, LabelFilter = LabelFilter.Null };
private List _individualKvWatchers = new List();
@@ -529,6 +530,7 @@ private static ConfigurationClientOptions GetDefaultClientOptions()
clientOptions.Retry.MaxRetries = MaxRetries;
clientOptions.Retry.MaxDelay = MaxRetryDelay;
clientOptions.Retry.Mode = RetryMode.Exponential;
+ clientOptions.Retry.NetworkTimeout = NetworkTimeout;
clientOptions.AddPolicy(new UserAgentHeaderPolicy(), HttpPipelinePosition.PerCall);
return clientOptions;
diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
index 28e2d507d..a83c74135 100644
--- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
+++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs
@@ -1232,6 +1232,11 @@ await ExecuteWithFailOverPolicyAsync