From 07d60c10dfdd907a8b5266e3eb10cc257e9c162f Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 22 Oct 2024 16:14:52 -0700 Subject: [PATCH 01/65] add connect Cdn --- .../AzureAppConfigurationOptions.cs | 14 ++++++++ .../EmptyTokenCredential.cs | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 7d9a9cad..4590977c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -301,6 +301,20 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString return this; } + /// + /// Connect the provider to CDN endpoint. + /// + /// The endpoint of the Azure App Configuration CDN to connect to. + public AzureAppConfigurationOptions ConnectCdn(Uri cdnEndpoint) + { + if (cdnEndpoint == null) + { + throw new ArgumentNullException(nameof(cdnEndpoint)); + } + + return Connect(cdnEndpoint, new EmptyTokenCredential()); + } + /// /// Connect the provider to Azure App Configuration using endpoint and token credentials. /// diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs new file mode 100644 index 00000000..ec347dd0 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs @@ -0,0 +1,35 @@ +using Azure.Core; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +{ + /// + /// A token credential that provides an empty token. + /// + public class EmptyTokenCredential : TokenCredential + { + /// + /// Gets an empty token. + /// + /// The context of the token request. + /// A cancellation token to cancel the operation. + /// An empty access token. + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new AccessToken(string.Empty, DateTimeOffset.MaxValue); + } + + /// + /// Asynchronously gets an empty token. + /// + /// The context of the token request. + /// A cancellation token to cancel the operation. + /// A task that represents the asynchronous operation. The task result contains an empty access token. + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + { + return new ValueTask(new AccessToken(string.Empty, DateTimeOffset.MaxValue)); + } + } +} From c2fd979dfc3bb9fb5494fa9ea702405452664a19 Mon Sep 17 00:00:00 2001 From: Sami Sadfa <71456174+samsadsam@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:39:20 -0700 Subject: [PATCH 02/65] Update src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs Co-authored-by: Avani Gupta --- .../AzureAppConfigurationOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 4590977c..a0c4638e 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -304,7 +304,7 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// /// Connect the provider to CDN endpoint. /// - /// The endpoint of the Azure App Configuration CDN to connect to. + /// The endpoint of the CDN instance to connect to. public AzureAppConfigurationOptions ConnectCdn(Uri cdnEndpoint) { if (cdnEndpoint == null) From d16a34422d1255ac745d2dd5ab5c34e0016489c2 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 22 Oct 2024 16:40:07 -0700 Subject: [PATCH 03/65] make internal --- .../EmptyTokenCredential.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs index ec347dd0..a68157d7 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration /// /// A token credential that provides an empty token. /// - public class EmptyTokenCredential : TokenCredential + internal class EmptyTokenCredential : TokenCredential { /// /// Gets an empty token. From a2e1c5d726b098b0d67ddcbd8c737fb5e0315b84 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 31 Oct 2024 13:51:35 -0700 Subject: [PATCH 04/65] add cdn client manager and api version policy --- .../AzureAppConfigurationOptions.cs | 15 ++-- .../AzureAppConfigurationProvider.cs | 2 +- .../CdnApiVersionPolicy.cs | 55 ++++++++++++++ .../CdnConfigurationClientManager.cs | 71 +++++++++++++++++++ .../ConfigurationClientExtensions.cs | 4 +- 5 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index b619615d..25c2f941 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -99,7 +99,6 @@ internal IEnumerable Adapters /// /// An optional configuration client manager that can be used to provide clients to communicate with Azure App Configuration. /// - /// This property is used only for unit testing. internal IConfigurationClientManager ClientManager { get; set; } /// @@ -321,15 +320,19 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// /// Connect the provider to CDN endpoint. /// - /// The endpoint of the CDN instance to connect to. - public AzureAppConfigurationOptions ConnectCdn(Uri cdnEndpoint) + /// The endpoint of the CDN instance to connect to. + public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) { - if (cdnEndpoint == null) + if (endpoint == null) { - throw new ArgumentNullException(nameof(cdnEndpoint)); + throw new ArgumentNullException(nameof(endpoint)); } - return Connect(cdnEndpoint, new EmptyTokenCredential()); + ClientOptions.AddPolicy(new CdnApiVersionPolicy(), HttpPipelinePosition.PerCall); + + ClientManager = new CdnConfigurationClientManager(new AzureAppConfigurationClientFactory(new EmptyTokenCredential(), ClientOptions), endpoint); + + return this; } /// diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index b5dd42f1..1a49ff06 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -282,7 +282,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => if (_watchedSettings.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv)) { await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken, !(_options.ClientManager is CdnConfigurationClientManager)).ConfigureAwait(false)).ConfigureAwait(false); } else { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs new file mode 100644 index 00000000..2d671272 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Azure.Core; +using Azure.Core.Pipeline; +using System; +using System.Collections.Specialized; +using System.Threading.Tasks; +using System.Web; + +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +{ + /// + /// A policy that adds the API version query parameter to HTTP requests. + /// + public class CdnApiVersionPolicy : HttpPipelinePolicy + { + /// + /// Processes the HTTP message by adding the API version query parameter. + /// + /// The HTTP message to process. + /// The pipeline of HTTP policies to apply. + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + message.Request.Uri.Reset(AlterApiVersion(message.Request.Uri.ToUri())); + + ProcessNext(message, pipeline); + } + + /// + /// Processes the HTTP message asynchronously by adding the API version query parameter. + /// + /// The HTTP message to process. + /// The pipeline of HTTP policies to apply. + /// A ValueTask representing the asynchronous operation. + public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + message.Request.Uri.Reset(AlterApiVersion(message.Request.Uri.ToUri())); + + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + + private static Uri AlterApiVersion(Uri uri) + { + var uriBuilder = new UriBuilder(uri); + + NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); + query["api-version"] = "2024-09-01-preview"; + + uriBuilder.Query = query.ToString(); + + return uriBuilder.Uri; + } + } +} diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs new file mode 100644 index 00000000..e98d84ba --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs @@ -0,0 +1,71 @@ +using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Azure; +using System; +using System.Collections.Generic; +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +{ + internal class CdnConfigurationClientManager : IConfigurationClientManager + { + private readonly IAzureClientFactory _clientFactory; + + private readonly ConfigurationClient _client; + private readonly Uri _endpoint; + + public CdnConfigurationClientManager( + IAzureClientFactory clientFactory, + Uri endpoint) + { + _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); + + _client = clientFactory.CreateClient(_endpoint.AbsoluteUri); + } + + public IEnumerable GetClients() + { + return new List { _client }; + } + + public void RefreshClients() + { + return; + } + + public bool UpdateSyncToken(Uri endpoint, string syncToken) + { + if (endpoint == null) + { + throw new ArgumentNullException(nameof(endpoint)); + } + + if (string.IsNullOrWhiteSpace(syncToken)) + { + throw new ArgumentNullException(nameof(syncToken)); + } + + if (new EndpointComparer().Equals(endpoint, _endpoint)) + { + _client.UpdateSyncToken(syncToken); + + return true; + } + + return false; + } + + public Uri GetEndpointForClient(ConfigurationClient client) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + if (client == _client) + { + return _endpoint; + } + + return null; + } + } +} diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index 4032b365..92faf6ad 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -16,7 +16,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions { internal static class ConfigurationClientExtensions { - public static async Task GetKeyValueChange(this ConfigurationClient client, ConfigurationSetting setting, CancellationToken cancellationToken) + public static async Task GetKeyValueChange(this ConfigurationClient client, ConfigurationSetting setting, CancellationToken cancellationToken, bool makeConditionalRequest = true) { if (setting == null) { @@ -30,7 +30,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli try { - Response response = await client.GetConfigurationSettingAsync(setting, onlyIfChanged: true, cancellationToken).ConfigureAwait(false); + Response response = await client.GetConfigurationSettingAsync(setting, onlyIfChanged: makeConditionalRequest, cancellationToken).ConfigureAwait(false); if (response.GetRawResponse().Status == (int)HttpStatusCode.OK && !response.Value.ETag.Equals(setting.ETag)) { From 9b2bcc97abd2c64cec6c96b06c21f1ce090e2453 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 31 Oct 2024 14:14:26 -0700 Subject: [PATCH 05/65] remove unused param --- .../CdnConfigurationClientManager.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs index e98d84ba..0da166a3 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs @@ -6,8 +6,6 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration { internal class CdnConfigurationClientManager : IConfigurationClientManager { - private readonly IAzureClientFactory _clientFactory; - private readonly ConfigurationClient _client; private readonly Uri _endpoint; @@ -15,7 +13,10 @@ public CdnConfigurationClientManager( IAzureClientFactory clientFactory, Uri endpoint) { - _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); + if (clientFactory == null) + { + throw new ArgumentNullException(nameof(clientFactory)); + } _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); _client = clientFactory.CreateClient(_endpoint.AbsoluteUri); From bf85303eceec808c3b3c8080e6118971c69eba6a Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 4 Nov 2024 09:36:20 -0800 Subject: [PATCH 06/65] fix format --- .../CdnConfigurationClientManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs index 0da166a3..8182cada 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs @@ -17,6 +17,7 @@ public CdnConfigurationClientManager( { throw new ArgumentNullException(nameof(clientFactory)); } + _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); _client = clientFactory.CreateClient(_endpoint.AbsoluteUri); From 7c4fddb85153143b567552bd1483a42cb9c602d7 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 4 Nov 2024 10:16:09 -0800 Subject: [PATCH 07/65] users cannot Connect/ConnectCdn at the same time --- .../AzureAppConfigurationOptions.cs | 20 +++++++++++--- .../AzureAppConfigurationSource.cs | 9 ++++++- .../CdnConfigurationClientManager.cs | 27 +++++++++---------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 25c2f941..fd81d061 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -99,6 +99,7 @@ internal IEnumerable Adapters /// /// An optional configuration client manager that can be used to provide clients to communicate with Azure App Configuration. /// + /// This property is used only for unit testing. internal IConfigurationClientManager ClientManager { get; set; } /// @@ -301,6 +302,11 @@ public AzureAppConfigurationOptions Connect(string connectionString) /// public AzureAppConfigurationOptions Connect(IEnumerable connectionStrings) { + if (Credential is EmptyTokenCredential) + { + throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); + } + if (connectionStrings == null || !connectionStrings.Any()) { throw new ArgumentNullException(nameof(connectionStrings)); @@ -323,6 +329,11 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// The endpoint of the CDN instance to connect to. public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) { + if (!(Credential is EmptyTokenCredential) || (ConnectionStrings?.Any() ?? false)) + { + throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); + } + if (endpoint == null) { throw new ArgumentNullException(nameof(endpoint)); @@ -330,9 +341,7 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) ClientOptions.AddPolicy(new CdnApiVersionPolicy(), HttpPipelinePosition.PerCall); - ClientManager = new CdnConfigurationClientManager(new AzureAppConfigurationClientFactory(new EmptyTokenCredential(), ClientOptions), endpoint); - - return this; + return Connect(new List() { endpoint }, new EmptyTokenCredential()); } /// @@ -362,6 +371,11 @@ public AzureAppConfigurationOptions Connect(Uri endpoint, TokenCredential creden /// Token credential to use to connect. public AzureAppConfigurationOptions Connect(IEnumerable endpoints, TokenCredential credential) { + if (Credential is EmptyTokenCredential) + { + throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); + } + if (endpoints == null || !endpoints.Any()) { throw new ArgumentNullException(nameof(endpoints)); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 83d20e2f..afe67c84 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -59,7 +59,14 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} to specify how to connect to Azure App Configuration."); } - provider = new AzureAppConfigurationProvider(new ConfigurationClientManager(clientFactory, endpoints, options.ReplicaDiscoveryEnabled, options.LoadBalancingEnabled), options, _optional); + if (options.Credential is EmptyTokenCredential) + { + provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints), options, _optional); + } + else + { + provider = new AzureAppConfigurationProvider(new ConfigurationClientManager(clientFactory, endpoints, options.ReplicaDiscoveryEnabled, options.LoadBalancingEnabled), options, _optional); + } } catch (InvalidOperationException ex) // InvalidOperationException is thrown when any problems are found while configuring AzureAppConfigurationOptions or when SDK fails to create a configurationClient. { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs index 8182cada..00a2cca1 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs @@ -2,30 +2,30 @@ using Microsoft.Extensions.Azure; using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration { internal class CdnConfigurationClientManager : IConfigurationClientManager { - private readonly ConfigurationClient _client; - private readonly Uri _endpoint; + private readonly IList _clients; public CdnConfigurationClientManager( IAzureClientFactory clientFactory, - Uri endpoint) + IEnumerable endpoints) { if (clientFactory == null) { throw new ArgumentNullException(nameof(clientFactory)); } - _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); - - _client = clientFactory.CreateClient(_endpoint.AbsoluteUri); + _clients = endpoints + .Select(endpoint => new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri))) + .ToList(); } public IEnumerable GetClients() { - return new List { _client }; + return _clients.Select(c => c.Client); } public void RefreshClients() @@ -45,9 +45,11 @@ public bool UpdateSyncToken(Uri endpoint, string syncToken) throw new ArgumentNullException(nameof(syncToken)); } - if (new EndpointComparer().Equals(endpoint, _endpoint)) + ConfigurationClientWrapper clientWrapper = _clients.SingleOrDefault(c => new EndpointComparer().Equals(c.Endpoint, endpoint)); + + if (clientWrapper != null) { - _client.UpdateSyncToken(syncToken); + clientWrapper.Client.UpdateSyncToken(syncToken); return true; } @@ -62,12 +64,9 @@ public Uri GetEndpointForClient(ConfigurationClient client) throw new ArgumentNullException(nameof(client)); } - if (client == _client) - { - return _endpoint; - } + ConfigurationClientWrapper currentClient = _clients.FirstOrDefault(c => c.Client == client); - return null; + return currentClient?.Endpoint; } } } From d323ecd4f90370f4cd9560bf20c6f397eb97ee26 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 4 Nov 2024 14:11:07 -0800 Subject: [PATCH 08/65] fix assertion --- .../AzureAppConfigurationProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 1a49ff06..f9b5b343 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -282,7 +282,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => if (_watchedSettings.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv)) { await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken, !(_options.ClientManager is CdnConfigurationClientManager)).ConfigureAwait(false)).ConfigureAwait(false); + async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken, !(_options.Credential is EmptyTokenCredential)).ConfigureAwait(false)).ConfigureAwait(false); } else { From a2ff1b1b82048a09bc824271a4db242d1dafc85b Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 7 Nov 2024 12:38:14 -0800 Subject: [PATCH 09/65] feedback --- .../AzureAppConfigurationOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index fd81d061..466e0bb9 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -329,7 +329,7 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// The endpoint of the CDN instance to connect to. public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) { - if (!(Credential is EmptyTokenCredential) || (ConnectionStrings?.Any() ?? false)) + if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) { throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); } @@ -339,7 +339,7 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) throw new ArgumentNullException(nameof(endpoint)); } - ClientOptions.AddPolicy(new CdnApiVersionPolicy(), HttpPipelinePosition.PerCall); + ClientOptions.AddPolicy(new CdnApiVersionPolicy(), HttpPipelinePosition.PerRetry); return Connect(new List() { endpoint }, new EmptyTokenCredential()); } From 5f1686769ffb4b6c3357609142b1fbe01ee18ba2 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 7 Nov 2024 12:46:42 -0800 Subject: [PATCH 10/65] add cdn tracing --- .../AzureAppConfigurationProvider.cs | 3 ++- .../Constants/RequestTracingConstants.cs | 1 + .../RequestTracingOptions.cs | 5 +++++ .../TracingUtils.cs | 5 +++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index f9b5b343..cc46d16f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -968,7 +968,8 @@ private void SetRequestTracingOptions() IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured, ReplicaCount = _options.Endpoints?.Count() - 1 ?? _options.ConnectionStrings?.Count() - 1 ?? 0, FeatureFlagTracing = _options.FeatureFlagTracing, - IsLoadBalancingEnabled = _options.LoadBalancingEnabled + IsLoadBalancingEnabled = _options.LoadBalancingEnabled, + IsCdnUsed = _options.Credential is EmptyTokenCredential }; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs index 15e862b6..4d7d7bdf 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs @@ -33,6 +33,7 @@ internal class RequestTracingConstants public const string LoadBalancingEnabledTag = "LB"; public const string SignalRUsedTag = "SignalR"; public const string FailoverRequestTag = "Failover"; + public const string CdnUsedTag = "CDN"; public const string FeatureFlagFilterTypeKey = "Filter"; public const string CustomFilter = "CSTM"; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs index 3838ee95..8c072835 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs @@ -63,6 +63,11 @@ internal class RequestTracingOptions /// public bool IsFailoverRequest { get; set; } = false; + /// + /// Flag to indicate wether the request is sent to a CDN. + /// + public bool IsCdnUsed { get; set; } = false; + /// /// Checks whether any tracing feature is used. /// diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs index b1b2b196..deb51835 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs @@ -201,6 +201,11 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re correlationContextTags.Add(RequestTracingConstants.FailoverRequestTag); } + if (requestTracingOptions.IsCdnUsed) + { + correlationContextTags.Add(RequestTracingConstants.CdnUsedTag); + } + var sb = new StringBuilder(); foreach (KeyValuePair kvp in correlationContextKeyValues) From 8fc28e89206d92e515f5e61c983763be825c8b9d Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 28 May 2025 12:24:53 -0700 Subject: [PATCH 11/65] remove api version policy --- .../AzureAppConfigurationOptions.cs | 2 - .../CdnApiVersionPolicy.cs | 55 ------------------- 2 files changed, 57 deletions(-) delete mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 35c286c9..6812a784 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -376,8 +376,6 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) throw new ArgumentNullException(nameof(endpoint)); } - ClientOptions.AddPolicy(new CdnApiVersionPolicy(), HttpPipelinePosition.PerRetry); - return Connect(new List() { endpoint }, new EmptyTokenCredential()); } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs deleted file mode 100644 index 2d671272..00000000 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnApiVersionPolicy.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Azure.Core; -using Azure.Core.Pipeline; -using System; -using System.Collections.Specialized; -using System.Threading.Tasks; -using System.Web; - -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration -{ - /// - /// A policy that adds the API version query parameter to HTTP requests. - /// - public class CdnApiVersionPolicy : HttpPipelinePolicy - { - /// - /// Processes the HTTP message by adding the API version query parameter. - /// - /// The HTTP message to process. - /// The pipeline of HTTP policies to apply. - public override void Process(HttpMessage message, ReadOnlyMemory pipeline) - { - message.Request.Uri.Reset(AlterApiVersion(message.Request.Uri.ToUri())); - - ProcessNext(message, pipeline); - } - - /// - /// Processes the HTTP message asynchronously by adding the API version query parameter. - /// - /// The HTTP message to process. - /// The pipeline of HTTP policies to apply. - /// A ValueTask representing the asynchronous operation. - public override async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) - { - message.Request.Uri.Reset(AlterApiVersion(message.Request.Uri.ToUri())); - - await ProcessNextAsync(message, pipeline).ConfigureAwait(false); - } - - private static Uri AlterApiVersion(Uri uri) - { - var uriBuilder = new UriBuilder(uri); - - NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); - query["api-version"] = "2024-09-01-preview"; - - uriBuilder.Query = query.ToString(); - - return uriBuilder.Uri; - } - } -} From 2d73ebbe5d24fcc124fa452c8ae6a8027151a1ef Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 28 May 2025 12:25:15 -0700 Subject: [PATCH 12/65] address comment --- .../CdnConfigurationClientManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs index 00a2cca1..94921972 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs @@ -1,4 +1,7 @@ -using Azure.Data.AppConfiguration; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; using System; using System.Collections.Generic; From ef09e7028cdda907ddb6de8dfb6b7e731f2fd3c7 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 28 May 2025 14:00:13 -0700 Subject: [PATCH 13/65] add cdn cache busting policy + accessor for sentinel keys --- .../AzureAppConfigurationOptions.cs | 11 +++ .../AzureAppConfigurationProvider.cs | 19 ++++- .../CdnCacheBustingAccessor.cs | 41 ++++++++++ .../CdnCacheBustingPolicy.cs | 77 +++++++++++++++++++ .../ICdnCacheBustingAccessor.cs | 16 ++++ 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingPolicy.cs create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ICdnCacheBustingAccessor.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 6812a784..3e4cc068 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -156,6 +156,12 @@ internal IEnumerable Adapters /// internal IAzureClientFactory ClientFactory { get; private set; } + /// + /// Accessor for CDN cache busting context that manages ETag injection into requests. + /// When null, CDN cache busting is disabled. When not null, CDN cache busting is enabled. + /// + internal ICdnCacheBustingAccessor CdnCacheBustingAccessor { get; private set; } + /// /// Initializes a new instance of the class. /// @@ -376,6 +382,11 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) throw new ArgumentNullException(nameof(endpoint)); } + CdnCacheBustingAccessor = new CdnCacheBustingAccessor(); + + // Add CDN cache busting policy to client options + ClientOptions.AddPolicy(new CdnCacheBustingPolicy(CdnCacheBustingAccessor), HttpPipelinePosition.PerCall); + return Connect(new List() { endpoint }, new EmptyTokenCredential()); } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 12488b83..de7788c2 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -949,6 +949,8 @@ private async Task RefreshIndividualKvWatchers( StringBuilder logInfoBuilder, CancellationToken cancellationToken) { + bool cdnMode = _options.CdnCacheBustingAccessor != null; + foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers) { string watchedKey = kvWatcher.Key; @@ -962,8 +964,13 @@ private async Task RefreshIndividualKvWatchers( // Find if there is a change associated with watcher if (_watchedIndividualKvs.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv)) { + if (cdnMode) + { + _options.CdnCacheBustingAccessor.CurrentETag = watchedKv.ETag.ToString(); + } + await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken, makeConditionalRequest: !cdnMode).ConfigureAwait(false)).ConfigureAwait(false); } else { @@ -1000,6 +1007,11 @@ await CallWithRequestTracing( if (kvWatcher.RefreshAll) { + if (cdnMode) + { + _options.CdnCacheBustingAccessor.CurrentETag = change.Current.ETag.ToString(); + } + return true; } } @@ -1009,6 +1021,11 @@ await CallWithRequestTracing( } } + if (cdnMode) + { + _options.CdnCacheBustingAccessor.CurrentETag = null; + } + return false; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs new file mode 100644 index 00000000..1d69db27 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using System.Threading; + +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +{ + /// + /// Implementation of ICdnCacheBustingAccessor that uses AsyncLocal for thread-safe context management. + /// + internal class CdnCacheBustingAccessor : ICdnCacheBustingAccessor + { + private static readonly AsyncLocal _context = new AsyncLocal(); + + /// + /// Gets or sets the current ETag value to be used for cache busting. + /// When null, CDN cache busting is disabled. When not null, the ETag will be injected into requests. + /// + public string CurrentETag + { + get => _context.Value?.ETag; + set => EnsureContext().ETag = value; + } + + private static CdnCacheBustingContext EnsureContext() + { + return _context.Value ??= new CdnCacheBustingContext(); + } + } + + /// + /// Context class that holds the CDN cache busting state. + /// + internal class CdnCacheBustingContext + { + /// + /// Gets or sets the ETag value for cache busting. + /// + public string ETag { get; set; } + } +} diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingPolicy.cs new file mode 100644 index 00000000..8e857fab --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingPolicy.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Azure.Core; +using Azure.Core.Pipeline; +using System; +using System.Diagnostics; +using System.Web; + +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +{ + /// + /// HTTP pipeline policy that injects ETags into the query string for CDN cache busting. + /// + internal class CdnCacheBustingPolicy : HttpPipelinePolicy + { + private readonly ICdnCacheBustingAccessor _cacheBustingAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// The CDN cache busting accessor. + public CdnCacheBustingPolicy(ICdnCacheBustingAccessor cacheBustingAccessor) + { + _cacheBustingAccessor = cacheBustingAccessor ?? throw new ArgumentNullException(nameof(cacheBustingAccessor)); + } + + /// + /// Processes the HTTP message and injects ETag into query string if CDN cache busting is enabled. + /// + /// The HTTP message. + /// The pipeline. + public override void Process(HttpMessage message, ReadOnlyMemory pipeline) + { + string etag = _cacheBustingAccessor.CurrentETag; + if (!string.IsNullOrEmpty(etag)) + { + // Add ETag to the request URI + message.Request.Uri.Reset(AddCacheBustingToUri(message.Request.Uri.ToUri(), etag)); + } + + ProcessNext(message, pipeline); + } + + /// + /// Processes the HTTP message asynchronously and injects ETag into query string if CDN cache busting is enabled. + /// + /// The HTTP message. + /// The pipeline. + /// A task representing the asynchronous operation. + public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) + { + string etag = _cacheBustingAccessor.CurrentETag; + if (!string.IsNullOrEmpty(etag)) + { + // Add ETag to the request URI + message.Request.Uri.Reset(AddCacheBustingToUri(message.Request.Uri.ToUri(), etag)); + } + + await ProcessNextAsync(message, pipeline).ConfigureAwait(false); + } + + private static Uri AddCacheBustingToUri(Uri uri, string etag) + { + Debug.Assert(!string.IsNullOrEmpty(etag)); + + var uriBuilder = new UriBuilder(uri); + + var query = HttpUtility.ParseQueryString(uriBuilder.Query); + query["cdn-cache-bust"] = etag; + + uriBuilder.Query = query.ToString(); + + return uriBuilder.Uri; + } + } +} diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ICdnCacheBustingAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ICdnCacheBustingAccessor.cs new file mode 100644 index 00000000..10f01df7 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ICdnCacheBustingAccessor.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +{ /// + /// Provides access to CDN cache busting context for managing ETag injection into HTTP requests. + /// + internal interface ICdnCacheBustingAccessor + { + /// + /// Gets or sets the current ETag value to be used for cache busting. + /// When null, CDN cache busting is disabled. When not null, the ETag will be injected into requests. + /// + string CurrentETag { get; set; } + } +} From b8ca58ed514385be220c86e54eecb1b0ac0a3001 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 29 May 2025 11:48:57 -0700 Subject: [PATCH 14/65] now refresh with sentinel key works (tested) --- .../AzureAppConfigurationProvider.cs | 11 +++-- .../Cdn/CdnCacheBustingAccessor.cs | 23 +++++++++++ .../{ => Cdn}/CdnCacheBustingPolicy.cs | 26 ++++++------ .../{ => Cdn}/EmptyTokenCredential.cs | 2 +- .../{ => Cdn}/ICdnCacheBustingAccessor.cs | 13 +++--- .../CdnCacheBustingAccessor.cs | 41 ------------------- 6 files changed, 51 insertions(+), 65 deletions(-) create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingAccessor.cs rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{ => Cdn}/CdnCacheBustingPolicy.cs (71%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{ => Cdn}/EmptyTokenCredential.cs (99%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{ => Cdn}/ICdnCacheBustingAccessor.cs (50%) delete mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index de7788c2..864c0a13 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -951,6 +951,11 @@ private async Task RefreshIndividualKvWatchers( { bool cdnMode = _options.CdnCacheBustingAccessor != null; + if (cdnMode) + { + _options.CdnCacheBustingAccessor.CurrentToken = null; + } + foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers) { string watchedKey = kvWatcher.Key; @@ -966,7 +971,7 @@ private async Task RefreshIndividualKvWatchers( { if (cdnMode) { - _options.CdnCacheBustingAccessor.CurrentETag = watchedKv.ETag.ToString(); + _options.CdnCacheBustingAccessor.CurrentToken ??= Guid.NewGuid().ToString(); } await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, @@ -1009,7 +1014,7 @@ await CallWithRequestTracing( { if (cdnMode) { - _options.CdnCacheBustingAccessor.CurrentETag = change.Current.ETag.ToString(); + _options.CdnCacheBustingAccessor.CurrentToken = change.Current.ETag.ToString(); } return true; @@ -1023,7 +1028,7 @@ await CallWithRequestTracing( if (cdnMode) { - _options.CdnCacheBustingAccessor.CurrentETag = null; + _options.CdnCacheBustingAccessor.CurrentToken = null; } return false; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingAccessor.cs new file mode 100644 index 00000000..5664d823 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingAccessor.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +{ + /// + /// Implementation of ICdnCacheBustingAccessor that manages the current token for cache busting. + /// + internal class CdnCacheBustingAccessor : ICdnCacheBustingAccessor + { + private string _currentToken; + + /// + /// Gets or sets the current token value to be used for cache busting. + /// When null, CDN cache busting is disabled. When not null, the token will be injected into requests. + /// + public string CurrentToken + { + get => _currentToken; + set => _currentToken = value; + } + } +} diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs similarity index 71% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingPolicy.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs index 8e857fab..1e1adb90 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingPolicy.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs @@ -19,55 +19,53 @@ internal class CdnCacheBustingPolicy : HttpPipelinePolicy /// /// Initializes a new instance of the class. /// - /// The CDN cache busting accessor. + /// The CDN cache busting accessor. public CdnCacheBustingPolicy(ICdnCacheBustingAccessor cacheBustingAccessor) { _cacheBustingAccessor = cacheBustingAccessor ?? throw new ArgumentNullException(nameof(cacheBustingAccessor)); } /// - /// Processes the HTTP message and injects ETag into query string if CDN cache busting is enabled. + /// Processes the HTTP message and injects token into query string if CDN cache busting is enabled. /// /// The HTTP message. /// The pipeline. public override void Process(HttpMessage message, ReadOnlyMemory pipeline) { - string etag = _cacheBustingAccessor.CurrentETag; - if (!string.IsNullOrEmpty(etag)) + string token = _cacheBustingAccessor.CurrentToken; + if (!string.IsNullOrEmpty(token)) { - // Add ETag to the request URI - message.Request.Uri.Reset(AddCacheBustingToUri(message.Request.Uri.ToUri(), etag)); + message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); } ProcessNext(message, pipeline); } /// - /// Processes the HTTP message asynchronously and injects ETag into query string if CDN cache busting is enabled. + /// Processes the HTTP message asynchronously and injects token into query string if CDN cache busting is enabled. /// /// The HTTP message. /// The pipeline. /// A task representing the asynchronous operation. public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) { - string etag = _cacheBustingAccessor.CurrentETag; - if (!string.IsNullOrEmpty(etag)) + string token = _cacheBustingAccessor.CurrentToken; + if (!string.IsNullOrEmpty(token)) { - // Add ETag to the request URI - message.Request.Uri.Reset(AddCacheBustingToUri(message.Request.Uri.ToUri(), etag)); + message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); } await ProcessNextAsync(message, pipeline).ConfigureAwait(false); } - private static Uri AddCacheBustingToUri(Uri uri, string etag) + private static Uri AddTokenToUri(Uri uri, string token) { - Debug.Assert(!string.IsNullOrEmpty(etag)); + Debug.Assert(!string.IsNullOrEmpty(token)); var uriBuilder = new UriBuilder(uri); var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query["cdn-cache-bust"] = etag; + query["cdn-cache-bust"] = token; uriBuilder.Query = query.ToString(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs similarity index 99% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs index a68157d7..e0167cde 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/EmptyTokenCredential.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs @@ -32,4 +32,4 @@ public override ValueTask GetTokenAsync(TokenRequestContext request return new ValueTask(new AccessToken(string.Empty, DateTimeOffset.MaxValue)); } } -} +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ICdnCacheBustingAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnCacheBustingAccessor.cs similarity index 50% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ICdnCacheBustingAccessor.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnCacheBustingAccessor.cs index 10f01df7..cd24323f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/ICdnCacheBustingAccessor.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnCacheBustingAccessor.cs @@ -2,15 +2,16 @@ // Licensed under the MIT license. // namespace Microsoft.Extensions.Configuration.AzureAppConfiguration -{ /// - /// Provides access to CDN cache busting context for managing ETag injection into HTTP requests. - /// +{ + /// + /// Provides access to CDN cache busting context for managing token injection into HTTP requests. + /// internal interface ICdnCacheBustingAccessor { /// - /// Gets or sets the current ETag value to be used for cache busting. - /// When null, CDN cache busting is disabled. When not null, the ETag will be injected into requests. + /// Gets or sets the current token value to be used for cache busting. + /// When null, CDN cache busting is disabled. When not null, the token will be injected into requests. /// - string CurrentETag { get; set; } + string CurrentToken { get; set; } } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs deleted file mode 100644 index 1d69db27..00000000 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnCacheBustingAccessor.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using System.Threading; - -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration -{ - /// - /// Implementation of ICdnCacheBustingAccessor that uses AsyncLocal for thread-safe context management. - /// - internal class CdnCacheBustingAccessor : ICdnCacheBustingAccessor - { - private static readonly AsyncLocal _context = new AsyncLocal(); - - /// - /// Gets or sets the current ETag value to be used for cache busting. - /// When null, CDN cache busting is disabled. When not null, the ETag will be injected into requests. - /// - public string CurrentETag - { - get => _context.Value?.ETag; - set => EnsureContext().ETag = value; - } - - private static CdnCacheBustingContext EnsureContext() - { - return _context.Value ??= new CdnCacheBustingContext(); - } - } - - /// - /// Context class that holds the CDN cache busting state. - /// - internal class CdnCacheBustingContext - { - /// - /// Gets or sets the ETag value for cache busting. - /// - public string ETag { get; set; } - } -} From f1e4bed28117d01e9dba846ff988c72cb6614c2a Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 29 May 2025 11:51:10 -0700 Subject: [PATCH 15/65] move cdn client manager under cdn folder --- .../{ => Cdn}/CdnConfigurationClientManager.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{ => Cdn}/CdnConfigurationClientManager.cs (100%) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs similarity index 100% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/CdnConfigurationClientManager.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs From 47a27eae856e35286c6492fbb100ecde4e791316 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 29 May 2025 11:54:21 -0700 Subject: [PATCH 16/65] add comment --- .../AzureAppConfigurationOptions.cs | 1 - .../AzureAppConfigurationProvider.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 3e4cc068..18895391 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -384,7 +384,6 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) CdnCacheBustingAccessor = new CdnCacheBustingAccessor(); - // Add CDN cache busting policy to client options ClientOptions.AddPolicy(new CdnCacheBustingPolicy(CdnCacheBustingAccessor), HttpPipelinePosition.PerCall); return Connect(new List() { endpoint }, new EmptyTokenCredential()); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 864c0a13..15dbae03 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -971,6 +971,8 @@ private async Task RefreshIndividualKvWatchers( { if (cdnMode) { + // + // use a random generated token to bust CDN cache _options.CdnCacheBustingAccessor.CurrentToken ??= Guid.NewGuid().ToString(); } From 491e948bcd048c10a64a9f8756058088e18d1405 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 29 May 2025 12:43:44 -0700 Subject: [PATCH 17/65] add collection monitoring cdn cache busting support --- .../AzureAppConfigurationProvider.cs | 42 +++++++++++++++---- .../ConfigurationClientExtensions.cs | 10 +++-- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 15dbae03..3f35a8f2 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -949,9 +949,9 @@ private async Task RefreshIndividualKvWatchers( StringBuilder logInfoBuilder, CancellationToken cancellationToken) { - bool cdnMode = _options.CdnCacheBustingAccessor != null; + bool isCdnEnabled = _options.CdnCacheBustingAccessor != null; - if (cdnMode) + if (isCdnEnabled) { _options.CdnCacheBustingAccessor.CurrentToken = null; } @@ -969,7 +969,7 @@ private async Task RefreshIndividualKvWatchers( // Find if there is a change associated with watcher if (_watchedIndividualKvs.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv)) { - if (cdnMode) + if (isCdnEnabled) { // // use a random generated token to bust CDN cache @@ -977,7 +977,7 @@ private async Task RefreshIndividualKvWatchers( } await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken, makeConditionalRequest: !cdnMode).ConfigureAwait(false)).ConfigureAwait(false); + async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken, makeConditionalRequest: !isCdnEnabled).ConfigureAwait(false)).ConfigureAwait(false); } else { @@ -1014,7 +1014,7 @@ await CallWithRequestTracing( if (kvWatcher.RefreshAll) { - if (cdnMode) + if (isCdnEnabled) { _options.CdnCacheBustingAccessor.CurrentToken = change.Current.ETag.ToString(); } @@ -1028,7 +1028,7 @@ await CallWithRequestTracing( } } - if (cdnMode) + if (isCdnEnabled) { _options.CdnCacheBustingAccessor.CurrentToken = null; } @@ -1359,26 +1359,54 @@ private async Task HaveCollectionsChanged( ConfigurationClient client, CancellationToken cancellationToken) { + bool isCdnEnabled = _options.CdnCacheBustingAccessor != null; + + if (isCdnEnabled) + { + _options.CdnCacheBustingAccessor.CurrentToken = null; + } + bool haveCollectionsChanged = false; + string changedPageEtag = null; foreach (KeyValueSelector selector in selectors) { + if (isCdnEnabled) + { + // + // use a random generated token to bust CDN cache + _options.CdnCacheBustingAccessor.CurrentToken ??= Guid.NewGuid().ToString(); + } + if (pageEtags.TryGetValue(selector, out IEnumerable matchConditions)) { await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => haveCollectionsChanged = await client.HaveCollectionsChanged( + async () => (haveCollectionsChanged, changedPageEtag) = await client.HaveCollectionsChanged( selector, matchConditions, _options.ConfigurationSettingPageIterator, + makeConditionalRequest: !isCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } if (haveCollectionsChanged) { + if (isCdnEnabled) + { + // + // If a page was deleted, we will use a random generated token since there is no changed etag + _options.CdnCacheBustingAccessor.CurrentToken = changedPageEtag ?? Guid.NewGuid().ToString(); + } + return true; } } + if (isCdnEnabled) + { + _options.CdnCacheBustingAccessor.CurrentToken = null; + } + return haveCollectionsChanged; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index 1dafeece..5e69e89b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -64,7 +64,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli }; } - public static async Task HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, CancellationToken cancellationToken) + public static async Task<(bool, string)> HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, bool makeConditionalRequest, CancellationToken cancellationToken) { if (matchConditions == null) { @@ -91,7 +91,9 @@ public static async Task HaveCollectionsChanged(this ConfigurationClient c using IEnumerator existingMatchConditionsEnumerator = matchConditions.GetEnumerator(); - await foreach (Page page in pageable.AsPages(pageIterator, matchConditions).ConfigureAwait(false)) + IAsyncEnumerable> pages = makeConditionalRequest ? pageable.AsPages(pageIterator, matchConditions) : pageable.AsPages(pageIterator); + + await foreach (Page page in pages.ConfigureAwait(false)) { using Response response = page.GetRawResponse(); @@ -100,12 +102,12 @@ public static async Task HaveCollectionsChanged(this ConfigurationClient c !existingMatchConditionsEnumerator.Current.IfNoneMatch.Equals(response.Headers.ETag)) && response.Status == (int)HttpStatusCode.OK) { - return true; + return (true, response.Headers.ETag.ToString()); } } // Need to check if pages were deleted and no change was found within the new shorter list of match conditions - return existingMatchConditionsEnumerator.MoveNext(); + return (existingMatchConditionsEnumerator.MoveNext(), null); } } } From 782dc33961c0230fdc84a44b6b911f18ed301f44 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 29 May 2025 18:29:53 -0700 Subject: [PATCH 18/65] change design to correct one --- .../AzureAppConfigurationProvider.cs | 47 ++----------------- .../ConfigurationClientExtensions.cs | 27 ++++++++--- 2 files changed, 24 insertions(+), 50 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 3f35a8f2..6dbdc0ee 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -951,11 +951,6 @@ private async Task RefreshIndividualKvWatchers( { bool isCdnEnabled = _options.CdnCacheBustingAccessor != null; - if (isCdnEnabled) - { - _options.CdnCacheBustingAccessor.CurrentToken = null; - } - foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers) { string watchedKey = kvWatcher.Key; @@ -971,13 +966,11 @@ private async Task RefreshIndividualKvWatchers( { if (isCdnEnabled) { - // - // use a random generated token to bust CDN cache - _options.CdnCacheBustingAccessor.CurrentToken ??= Guid.NewGuid().ToString(); + _options.CdnCacheBustingAccessor.CurrentToken = watchedKv.ETag.ToString(); } await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, cancellationToken, makeConditionalRequest: !isCdnEnabled).ConfigureAwait(false)).ConfigureAwait(false); + async () => change = await client.GetKeyValueChange(watchedKv, makeConditionalRequest: !isCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } else { @@ -1028,11 +1021,6 @@ await CallWithRequestTracing( } } - if (isCdnEnabled) - { - _options.CdnCacheBustingAccessor.CurrentToken = null; - } - return false; } @@ -1359,54 +1347,27 @@ private async Task HaveCollectionsChanged( ConfigurationClient client, CancellationToken cancellationToken) { - bool isCdnEnabled = _options.CdnCacheBustingAccessor != null; - - if (isCdnEnabled) - { - _options.CdnCacheBustingAccessor.CurrentToken = null; - } - bool haveCollectionsChanged = false; - string changedPageEtag = null; foreach (KeyValueSelector selector in selectors) { - if (isCdnEnabled) - { - // - // use a random generated token to bust CDN cache - _options.CdnCacheBustingAccessor.CurrentToken ??= Guid.NewGuid().ToString(); - } - if (pageEtags.TryGetValue(selector, out IEnumerable matchConditions)) { await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => (haveCollectionsChanged, changedPageEtag) = await client.HaveCollectionsChanged( + async () => haveCollectionsChanged = await client.HaveCollectionsChanged( selector, matchConditions, _options.ConfigurationSettingPageIterator, - makeConditionalRequest: !isCdnEnabled, + _options.CdnCacheBustingAccessor, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } if (haveCollectionsChanged) { - if (isCdnEnabled) - { - // - // If a page was deleted, we will use a random generated token since there is no changed etag - _options.CdnCacheBustingAccessor.CurrentToken = changedPageEtag ?? Guid.NewGuid().ToString(); - } - return true; } } - if (isCdnEnabled) - { - _options.CdnCacheBustingAccessor.CurrentToken = null; - } - return haveCollectionsChanged; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index 5e69e89b..9e94ea97 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions { internal static class ConfigurationClientExtensions { - public static async Task GetKeyValueChange(this ConfigurationClient client, ConfigurationSetting setting, CancellationToken cancellationToken, bool makeConditionalRequest = true) + public static async Task GetKeyValueChange(this ConfigurationClient client, ConfigurationSetting setting, bool makeConditionalRequest, CancellationToken cancellationToken) { if (setting == null) { @@ -64,7 +64,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli }; } - public static async Task<(bool, string)> HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, bool makeConditionalRequest, CancellationToken cancellationToken) + public static async Task HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, ICdnCacheBustingAccessor cdnCacheBustingAccessor, CancellationToken cancellationToken) { if (matchConditions == null) { @@ -91,23 +91,36 @@ public static async Task GetKeyValueChange(this ConfigurationCli using IEnumerator existingMatchConditionsEnumerator = matchConditions.GetEnumerator(); - IAsyncEnumerable> pages = makeConditionalRequest ? pageable.AsPages(pageIterator, matchConditions) : pageable.AsPages(pageIterator); + bool isCdnEnabled = cdnCacheBustingAccessor != null; + + IAsyncEnumerable> pages = isCdnEnabled ? pageable.AsPages() : pageable.AsPages(pageIterator, matchConditions); await foreach (Page page in pages.ConfigureAwait(false)) { using Response response = page.GetRawResponse(); - // Return true if the lists of etags are different - if ((!existingMatchConditionsEnumerator.MoveNext() || + bool canPeek = existingMatchConditionsEnumerator.MoveNext(); + + if ((!canPeek || !existingMatchConditionsEnumerator.Current.IfNoneMatch.Equals(response.Headers.ETag)) && response.Status == (int)HttpStatusCode.OK) { - return (true, response.Headers.ETag.ToString()); + if (isCdnEnabled) + { + cdnCacheBustingAccessor.CurrentToken = response.Headers.ETag.ToString(); + } + + return true; + } + + if (isCdnEnabled && canPeek) + { + cdnCacheBustingAccessor.CurrentToken = existingMatchConditionsEnumerator.Current.IfNoneMatch.ToString(); } } // Need to check if pages were deleted and no change was found within the new shorter list of match conditions - return (existingMatchConditionsEnumerator.MoveNext(), null); + return existingMatchConditionsEnumerator.MoveNext(); } } } From dec256b3b3182212bc59515e76fb70141c2495de Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 29 May 2025 18:35:33 -0700 Subject: [PATCH 19/65] bug fix --- .../Extensions/ConfigurationClientExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index 9e94ea97..73bada96 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -95,12 +95,12 @@ public static async Task HaveCollectionsChanged(this ConfigurationClient c IAsyncEnumerable> pages = isCdnEnabled ? pageable.AsPages() : pageable.AsPages(pageIterator, matchConditions); + bool canPeek = existingMatchConditionsEnumerator.MoveNext(); + await foreach (Page page in pages.ConfigureAwait(false)) { using Response response = page.GetRawResponse(); - bool canPeek = existingMatchConditionsEnumerator.MoveNext(); - if ((!canPeek || !existingMatchConditionsEnumerator.Current.IfNoneMatch.Equals(response.Headers.ETag)) && response.Status == (int)HttpStatusCode.OK) @@ -113,6 +113,8 @@ public static async Task HaveCollectionsChanged(this ConfigurationClient c return true; } + canPeek = existingMatchConditionsEnumerator.MoveNext(); + if (isCdnEnabled && canPeek) { cdnCacheBustingAccessor.CurrentToken = existingMatchConditionsEnumerator.Current.IfNoneMatch.ToString(); From 186288bfe98f5feb84113150e8a08ce0c567c35c Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 29 May 2025 18:39:18 -0700 Subject: [PATCH 20/65] another bug fix --- .../Extensions/ConfigurationClientExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index 73bada96..3e98963c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -97,6 +97,11 @@ public static async Task HaveCollectionsChanged(this ConfigurationClient c bool canPeek = existingMatchConditionsEnumerator.MoveNext(); + if (isCdnEnabled && canPeek) + { + cdnCacheBustingAccessor.CurrentToken = existingMatchConditionsEnumerator.Current.IfNoneMatch.ToString(); + } + await foreach (Page page in pages.ConfigureAwait(false)) { using Response response = page.GetRawResponse(); From dbfa697705c1555669496c23232ea59564220e58 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 2 Jun 2025 22:42:26 -0700 Subject: [PATCH 21/65] iterate and implement new design --- .../AzureAppConfigurationOptions.cs | 5 + .../AzureAppConfigurationProvider.cs | 142 ++++++++++++++---- .../AzureAppConfigurationSource.cs | 4 +- .../Cdn/CdnCacheBustingPolicy.cs | 4 +- .../ConfigurationClientExtensions.cs | 32 +--- 5 files changed, 126 insertions(+), 61 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 18895391..0d7fa9ed 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -162,6 +162,11 @@ internal IEnumerable Adapters /// internal ICdnCacheBustingAccessor CdnCacheBustingAccessor { get; private set; } + /// + /// Gets a value indicating whether CDN is enabled. + /// + internal bool IsCdnEnabled => CdnCacheBustingAccessor != null; + /// /// Initializes a new instance of the class. /// diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 6dbdc0ee..ec61cef1 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -15,6 +15,7 @@ using System.Net.Http; using System.Net.Sockets; using System.Security; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -39,6 +40,11 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura private Dictionary _configClientBackoffs = new Dictionary(); private DateTimeOffset _nextCollectionRefreshTime; + #region Cdn + private string _configVersion = null; + private string _ffCollectionVersion = null; + #endregion + private readonly TimeSpan MinRefreshInterval; // The most-recent time when the refresh operation attempted to load the initial configuration @@ -280,8 +286,8 @@ public async Task RefreshAsync(CancellationToken cancellationToken) List keyValueChanges = null; Dictionary data = null; Dictionary ffCollectionData = null; - bool ffCollectionUpdated = false; - bool refreshAll = false; + string ffCollectionUpdatedChangedEtag = null; + string refreshAllChangedEtag = null; StringBuilder logInfoBuilder = new StringBuilder(); StringBuilder logDebugBuilder = new StringBuilder(); @@ -294,8 +300,8 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => keyValueChanges = new List(); data = null; ffCollectionData = null; - ffCollectionUpdated = false; - refreshAll = false; + ffCollectionUpdatedChangedEtag = null; + refreshAllChangedEtag = null; logDebugBuilder.Clear(); logInfoBuilder.Clear(); Uri endpoint = _configClientManager.GetEndpointForClient(client); @@ -305,7 +311,17 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => // Get key value collection changes if RegisterAll was called if (isRefreshDue) { - refreshAll = await HaveCollectionsChanged( + if (_options.IsCdnEnabled) + { + if (_configVersion == null && _kvEtags.Count > 0) + { + _configVersion = AzureAppConfigurationProvider.CalculateHash(_kvEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); + } + + _options.CdnCacheBustingAccessor.CurrentToken = _configVersion; + } + + refreshAllChangedEtag = await HaveCollectionsChanged( _options.Selectors.Where(selector => !selector.IsFeatureFlagSelector), _kvEtags, client, @@ -314,7 +330,17 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => } else { - refreshAll = await RefreshIndividualKvWatchers( + if (_options.IsCdnEnabled) + { + if (_configVersion == null && _watchedIndividualKvs.Count > 0) + { + _configVersion = AzureAppConfigurationProvider.CalculateHash(_watchedIndividualKvs.Select(kvp => kvp.Value.ETag.ToString())); + } + + _options.CdnCacheBustingAccessor.CurrentToken = _configVersion; + } + + refreshAllChangedEtag = await RefreshIndividualKvWatchers( client, keyValueChanges, refreshableIndividualKvWatchers, @@ -324,7 +350,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => cancellationToken).ConfigureAwait(false); } - if (refreshAll) + if (refreshAllChangedEtag != null) { // Trigger a single load-all operation if a change was detected in one or more key-values with refreshAll: true, // or if any key-value collection change was detected. @@ -332,6 +358,17 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => ffEtags = new Dictionary>(); ffKeys = new HashSet(); + if (_options.IsCdnEnabled) + { + // + // Bust cdn cache + _options.CdnCacheBustingAccessor.CurrentToken = refreshAllChangedEtag; + + // Reset versions so that next watch request will not use stale versions. + _configVersion = null; + _ffCollectionVersion = null; + } + data = await LoadSelected(client, kvEtags, ffEtags, _options.Selectors, ffKeys, cancellationToken).ConfigureAwait(false); watchedIndividualKvs = await LoadKeyValuesRegisteredForRefresh(client, data, cancellationToken).ConfigureAwait(false); logInfoBuilder.AppendLine(LogHelper.BuildConfigurationUpdatedMessage()); @@ -339,7 +376,17 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => } // Get feature flag changes - ffCollectionUpdated = await HaveCollectionsChanged( + if (_options.IsCdnEnabled) + { + if (_ffCollectionVersion == null && _ffEtags.Count > 0) + { + _ffCollectionVersion = AzureAppConfigurationProvider.CalculateHash(_ffEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); + } + + _options.CdnCacheBustingAccessor.CurrentToken = _ffCollectionVersion; + } + + ffCollectionUpdatedChangedEtag = await HaveCollectionsChanged( refreshableFfWatchers.Select(watcher => new KeyValueSelector { KeyFilter = watcher.Key, @@ -350,11 +397,20 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => client, cancellationToken).ConfigureAwait(false); - if (ffCollectionUpdated) + if (ffCollectionUpdatedChangedEtag != null) { ffEtags = new Dictionary>(); ffKeys = new HashSet(); + if (_options.IsCdnEnabled) + { + // + // Bust cdn cache + _options.CdnCacheBustingAccessor.CurrentToken = ffCollectionUpdatedChangedEtag; + // Reset ff collection version so that next ff watch request will not use stale version. + _ffCollectionVersion = null; + } + ffCollectionData = await LoadSelected( client, new Dictionary>(), @@ -373,6 +429,9 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => cancellationToken) .ConfigureAwait(false); + bool refreshAll = !string.IsNullOrEmpty(refreshAllChangedEtag); + bool ffCollectionUpdated = !string.IsNullOrEmpty(ffCollectionUpdatedChangedEtag); + if (refreshAll) { _mappedData = await MapConfigurationSettings(data).ConfigureAwait(false); @@ -940,7 +999,7 @@ private async Task> LoadKey return watchedIndividualKvs; } - private async Task RefreshIndividualKvWatchers( + private async Task RefreshIndividualKvWatchers( ConfigurationClient client, List keyValueChanges, IEnumerable refreshableIndividualKvWatchers, @@ -949,8 +1008,6 @@ private async Task RefreshIndividualKvWatchers( StringBuilder logInfoBuilder, CancellationToken cancellationToken) { - bool isCdnEnabled = _options.CdnCacheBustingAccessor != null; - foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers) { string watchedKey = kvWatcher.Key; @@ -964,13 +1021,8 @@ private async Task RefreshIndividualKvWatchers( // Find if there is a change associated with watcher if (_watchedIndividualKvs.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv)) { - if (isCdnEnabled) - { - _options.CdnCacheBustingAccessor.CurrentToken = watchedKv.ETag.ToString(); - } - await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, makeConditionalRequest: !isCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + async () => change = await client.GetKeyValueChange(watchedKv, makeConditionalRequest: !_options.IsCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } else { @@ -1007,12 +1059,14 @@ await CallWithRequestTracing( if (kvWatcher.RefreshAll) { - if (isCdnEnabled) - { - _options.CdnCacheBustingAccessor.CurrentToken = change.Current.ETag.ToString(); - } + return change.Current.ETag.ToString(); + } - return true; + if (_options.IsCdnEnabled) + { + // + // even if the change is not refresh all, we still need to reset stale version. + _configVersion = null; } } else @@ -1021,7 +1075,7 @@ await CallWithRequestTracing( } } - return false; + return null; } private void SetData(IDictionary data) @@ -1341,34 +1395,60 @@ private void UpdateClientBackoffStatus(Uri endpoint, bool successful) _configClientBackoffs[endpoint] = clientBackoffStatus; } - private async Task HaveCollectionsChanged( + private async Task HaveCollectionsChanged( IEnumerable selectors, Dictionary> pageEtags, ConfigurationClient client, CancellationToken cancellationToken) { - bool haveCollectionsChanged = false; + string changedEtag = null; foreach (KeyValueSelector selector in selectors) { if (pageEtags.TryGetValue(selector, out IEnumerable matchConditions)) { await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => haveCollectionsChanged = await client.HaveCollectionsChanged( + async () => changedEtag = await client.HaveCollectionsChanged( selector, matchConditions, _options.ConfigurationSettingPageIterator, - _options.CdnCacheBustingAccessor, + makeConditionalRequest: !_options.IsCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } - if (haveCollectionsChanged) + if (changedEtag != null) { - return true; + // If we have a changed ETag, we can stop checking further selectors + return changedEtag; } } - return haveCollectionsChanged; + return changedEtag; + } + + private static string CalculateHash(IEnumerable etags) + { + Debug.Assert(etags != null && etags.Any()); + + StringBuilder inputBuilder = new StringBuilder(); + + foreach (string etag in etags) + { + inputBuilder.Append(etag); + inputBuilder.Append('\n'); + } + + // Remove the last newline character + if (inputBuilder.Length > 0) + { + inputBuilder.Length--; + } + + string input = inputBuilder.ToString(); + + using SHA256 sha256 = SHA256.Create(); + + return sha256.ComputeHash(Encoding.UTF8.GetBytes(input)).ToBase64Url(); } private async Task ProcessKeyValueChangesAsync( diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index afe67c84..23db4631 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -56,10 +56,10 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } else { - throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} to specify how to connect to Azure App Configuration."); + throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} or {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.ConnectCdn)} to specify how to connect to Azure App Configuration."); } - if (options.Credential is EmptyTokenCredential) + if (options.IsCdnEnabled) { provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints), options, _optional); } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs index 1e1adb90..394fd845 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration { /// - /// HTTP pipeline policy that injects ETags into the query string for CDN cache busting. + /// HTTP pipeline policy that injects token into the query string for CDN cache busting. /// internal class CdnCacheBustingPolicy : HttpPipelinePolicy { @@ -65,7 +65,7 @@ private static Uri AddTokenToUri(Uri uri, string token) var uriBuilder = new UriBuilder(uri); var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query["cdn-cache-bust"] = token; + query["_"] = token; uriBuilder.Query = query.ToString(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index 3e98963c..c51cf57a 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -64,7 +64,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli }; } - public static async Task HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, ICdnCacheBustingAccessor cdnCacheBustingAccessor, CancellationToken cancellationToken) + public static async Task HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, bool makeConditionalRequest, CancellationToken cancellationToken) { if (matchConditions == null) { @@ -91,43 +91,23 @@ public static async Task HaveCollectionsChanged(this ConfigurationClient c using IEnumerator existingMatchConditionsEnumerator = matchConditions.GetEnumerator(); - bool isCdnEnabled = cdnCacheBustingAccessor != null; - - IAsyncEnumerable> pages = isCdnEnabled ? pageable.AsPages() : pageable.AsPages(pageIterator, matchConditions); - - bool canPeek = existingMatchConditionsEnumerator.MoveNext(); - - if (isCdnEnabled && canPeek) - { - cdnCacheBustingAccessor.CurrentToken = existingMatchConditionsEnumerator.Current.IfNoneMatch.ToString(); - } + IAsyncEnumerable> pages = makeConditionalRequest ? pageable.AsPages(pageIterator, matchConditions) : pageable.AsPages(pageIterator); await foreach (Page page in pages.ConfigureAwait(false)) { using Response response = page.GetRawResponse(); - if ((!canPeek || + // Check if the ETag has changed + if ((!existingMatchConditionsEnumerator.MoveNext() || !existingMatchConditionsEnumerator.Current.IfNoneMatch.Equals(response.Headers.ETag)) && response.Status == (int)HttpStatusCode.OK) { - if (isCdnEnabled) - { - cdnCacheBustingAccessor.CurrentToken = response.Headers.ETag.ToString(); - } - - return true; - } - - canPeek = existingMatchConditionsEnumerator.MoveNext(); - - if (isCdnEnabled && canPeek) - { - cdnCacheBustingAccessor.CurrentToken = existingMatchConditionsEnumerator.Current.IfNoneMatch.ToString(); + return response.Headers.ETag.ToString(); } } // Need to check if pages were deleted and no change was found within the new shorter list of match conditions - return existingMatchConditionsEnumerator.MoveNext(); + return existingMatchConditionsEnumerator.MoveNext() ? existingMatchConditionsEnumerator.Current.IfNoneMatch.ToString() : null; } } } From cfc83ce0a0556d1ef1dfaedc5ea7f50cd4f22fbf Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 2 Jun 2025 22:57:27 -0700 Subject: [PATCH 22/65] make code clearer --- .../AzureAppConfigurationOptions.cs | 16 +++++++++++----- .../AzureAppConfigurationProvider.cs | 2 +- .../Constants/RequestTracingConstants.cs | 2 +- .../RequestTracingOptions.cs | 2 +- .../TracingUtils.cs | 4 ++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 0d7fa9ed..512a2f64 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -350,7 +350,7 @@ public AzureAppConfigurationOptions Connect(string connectionString) /// public AzureAppConfigurationOptions Connect(IEnumerable connectionStrings) { - if (Credential is EmptyTokenCredential) + if (IsCdnEnabled) { throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); } @@ -377,7 +377,12 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// The endpoint of the CDN instance to connect to. public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) { - if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) + if (IsCdnEnabled) + { + throw new InvalidOperationException("Please call ConnectCdn() only once."); + } + + if ((Credential != null) || (ConnectionStrings?.Any() ?? false)) { throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); } @@ -387,11 +392,12 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) throw new ArgumentNullException(nameof(endpoint)); } - CdnCacheBustingAccessor = new CdnCacheBustingAccessor(); + var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); + CdnCacheBustingAccessor = new CdnCacheBustingAccessor(); ClientOptions.AddPolicy(new CdnCacheBustingPolicy(CdnCacheBustingAccessor), HttpPipelinePosition.PerCall); - return Connect(new List() { endpoint }, new EmptyTokenCredential()); + return result; } /// @@ -421,7 +427,7 @@ public AzureAppConfigurationOptions Connect(Uri endpoint, TokenCredential creden /// Token credential to use to connect. public AzureAppConfigurationOptions Connect(IEnumerable endpoints, TokenCredential credential) { - if (Credential is EmptyTokenCredential) + if (IsCdnEnabled) { throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index ec61cef1..0fe6155b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1132,7 +1132,7 @@ private void SetRequestTracingOptions() IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured, FeatureFlagTracing = _options.FeatureFlagTracing, IsLoadBalancingEnabled = _options.LoadBalancingEnabled, - IsCdnUsed = _options.Credential is EmptyTokenCredential + IsCdnEnabled = _options.IsCdnEnabled }; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs index ebc0b79d..5c007f74 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs @@ -37,7 +37,7 @@ internal class RequestTracingConstants public const string SignalRUsedTag = "SignalR"; public const string FailoverRequestTag = "Failover"; public const string PushRefreshTag = "PushRefresh"; - public const string CdnUsedTag = "CDN"; + public const string CdnTag = "CDN"; public const string FeatureFlagFilterTypeKey = "Filter"; public const string CustomFilter = "CSTM"; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs index 5a1ad289..85f1ab1c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs @@ -73,7 +73,7 @@ internal class RequestTracingOptions /// /// Flag to indicate wether the request is sent to a CDN. /// - public bool IsCdnUsed { get; set; } = false; + public bool IsCdnEnabled { get; set; } = false; /// /// Flag to indicate whether any key-value uses the json content type and contains diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs index c6c8a74c..99a6604d 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs @@ -206,9 +206,9 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re correlationContextTags.Add(RequestTracingConstants.PushRefreshTag); } - if (requestTracingOptions.IsCdnUsed) + if (requestTracingOptions.IsCdnEnabled) { - correlationContextTags.Add(RequestTracingConstants.CdnUsedTag); + correlationContextTags.Add(RequestTracingConstants.CdnTag); } var sb = new StringBuilder(); From 36317310850d2892cf98bcdb1b7434ca871e8633 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 2 Jun 2025 23:46:41 -0700 Subject: [PATCH 23/65] nit --- .../AzureAppConfigurationProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 0fe6155b..c1c4ff4e 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -315,7 +315,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_configVersion == null && _kvEtags.Count > 0) { - _configVersion = AzureAppConfigurationProvider.CalculateHash(_kvEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); + _configVersion = CalculateHash(_kvEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); } _options.CdnCacheBustingAccessor.CurrentToken = _configVersion; @@ -334,7 +334,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_configVersion == null && _watchedIndividualKvs.Count > 0) { - _configVersion = AzureAppConfigurationProvider.CalculateHash(_watchedIndividualKvs.Select(kvp => kvp.Value.ETag.ToString())); + _configVersion = CalculateHash(_watchedIndividualKvs.Select(kvp => kvp.Value.ETag.ToString())); } _options.CdnCacheBustingAccessor.CurrentToken = _configVersion; @@ -380,7 +380,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_ffCollectionVersion == null && _ffEtags.Count > 0) { - _ffCollectionVersion = AzureAppConfigurationProvider.CalculateHash(_ffEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); + _ffCollectionVersion = CalculateHash(_ffEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); } _options.CdnCacheBustingAccessor.CurrentToken = _ffCollectionVersion; From 2a7f79bc47369280f865e578687c46307a72fc36 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 3 Jun 2025 12:08:57 -0700 Subject: [PATCH 24/65] add purpose and simplify hash function --- .../AzureAppConfigurationProvider.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index c1c4ff4e..cc96788f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1432,23 +1432,19 @@ private static string CalculateHash(IEnumerable etags) StringBuilder inputBuilder = new StringBuilder(); + // + // purpose + inputBuilder.Append("etags\n"); + foreach (string etag in etags) { inputBuilder.Append(etag); inputBuilder.Append('\n'); } - // Remove the last newline character - if (inputBuilder.Length > 0) - { - inputBuilder.Length--; - } - - string input = inputBuilder.ToString(); - using SHA256 sha256 = SHA256.Create(); - return sha256.ComputeHash(Encoding.UTF8.GetBytes(input)).ToBase64Url(); + return sha256.ComputeHash(Encoding.UTF8.GetBytes(inputBuilder.ToString())).ToBase64Url(); } private async Task ProcessKeyValueChangesAsync( From d78de74b011fb2239a3bbbfc6d0f896b4da95102 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 3 Jun 2025 15:19:33 -0700 Subject: [PATCH 25/65] nit: cleanup and simplification --- .../AzureAppConfigurationOptions.cs | 11 ++--- .../AzureAppConfigurationProvider.cs | 43 ++++++++----------- .../AzureAppConfigurationSource.cs | 2 +- ...ingPolicy.cs => CacheConsistencyPolicy.cs} | 24 +++++------ ...or.cs => CacheConsistencyTokenAccessor.cs} | 12 +++--- ...nager.cs => ConfigurationClientManager.cs} | 6 +-- .../Cdn/EmptyTokenCredential.cs | 2 +- ...r.cs => ICacheConsistencyTokenAccessor.cs} | 16 +++---- 8 files changed, 54 insertions(+), 62 deletions(-) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{CdnCacheBustingPolicy.cs => CacheConsistencyPolicy.cs} (68%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{CdnCacheBustingAccessor.cs => CacheConsistencyTokenAccessor.cs} (60%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{CdnConfigurationClientManager.cs => ConfigurationClientManager.cs} (94%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{ICdnCacheBustingAccessor.cs => ICacheConsistencyTokenAccessor.cs} (50%) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 512a2f64..0b62fafb 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -6,6 +6,7 @@ using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models; @@ -158,14 +159,14 @@ internal IEnumerable Adapters /// /// Accessor for CDN cache busting context that manages ETag injection into requests. - /// When null, CDN cache busting is disabled. When not null, CDN cache busting is enabled. + /// When null, CDN cache consistency is disabled. When not null, CDN cache consistency is enabled. /// - internal ICdnCacheBustingAccessor CdnCacheBustingAccessor { get; private set; } + internal ICacheConsistencyTokenAccessor CacheConsistencyTokenAccessor { get; private set; } /// /// Gets a value indicating whether CDN is enabled. /// - internal bool IsCdnEnabled => CdnCacheBustingAccessor != null; + internal bool IsCdnEnabled => CacheConsistencyTokenAccessor != null; /// /// Initializes a new instance of the class. @@ -394,8 +395,8 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); - CdnCacheBustingAccessor = new CdnCacheBustingAccessor(); - ClientOptions.AddPolicy(new CdnCacheBustingPolicy(CdnCacheBustingAccessor), HttpPipelinePosition.PerCall); + CacheConsistencyTokenAccessor = new CacheConsistencyTokenAccessor(); + ClientOptions.AddPolicy(new CacheConsistencyPolicy(CacheConsistencyTokenAccessor), HttpPipelinePosition.PerCall); return result; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index cc96788f..b8f6825c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -313,12 +313,8 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_options.IsCdnEnabled) { - if (_configVersion == null && _kvEtags.Count > 0) - { - _configVersion = CalculateHash(_kvEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); - } - - _options.CdnCacheBustingAccessor.CurrentToken = _configVersion; + _configVersion ??= CalculateCacheConsistencyToken(_kvEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); + _options.CacheConsistencyTokenAccessor.Current = _configVersion; } refreshAllChangedEtag = await HaveCollectionsChanged( @@ -332,12 +328,8 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_options.IsCdnEnabled) { - if (_configVersion == null && _watchedIndividualKvs.Count > 0) - { - _configVersion = CalculateHash(_watchedIndividualKvs.Select(kvp => kvp.Value.ETag.ToString())); - } - - _options.CdnCacheBustingAccessor.CurrentToken = _configVersion; + _configVersion ??= CalculateCacheConsistencyToken(_watchedIndividualKvs.Select(kvp => kvp.Value.ETag.ToString())); + _options.CacheConsistencyTokenAccessor.Current = _configVersion; } refreshAllChangedEtag = await RefreshIndividualKvWatchers( @@ -361,9 +353,10 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => if (_options.IsCdnEnabled) { // - // Bust cdn cache - _options.CdnCacheBustingAccessor.CurrentToken = refreshAllChangedEtag; + // Break cdn cache + _options.CacheConsistencyTokenAccessor.Current = refreshAllChangedEtag; + // // Reset versions so that next watch request will not use stale versions. _configVersion = null; _ffCollectionVersion = null; @@ -378,12 +371,8 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => // Get feature flag changes if (_options.IsCdnEnabled) { - if (_ffCollectionVersion == null && _ffEtags.Count > 0) - { - _ffCollectionVersion = CalculateHash(_ffEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); - } - - _options.CdnCacheBustingAccessor.CurrentToken = _ffCollectionVersion; + _ffCollectionVersion ??= CalculateCacheConsistencyToken(_ffEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); + _options.CacheConsistencyTokenAccessor.Current = _ffCollectionVersion; } ffCollectionUpdatedChangedEtag = await HaveCollectionsChanged( @@ -405,8 +394,10 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => if (_options.IsCdnEnabled) { // - // Bust cdn cache - _options.CdnCacheBustingAccessor.CurrentToken = ffCollectionUpdatedChangedEtag; + // Break cdn cache + _options.CacheConsistencyTokenAccessor.Current = ffCollectionUpdatedChangedEtag; + + // // Reset ff collection version so that next ff watch request will not use stale version. _ffCollectionVersion = null; } @@ -1065,7 +1056,7 @@ await CallWithRequestTracing( if (_options.IsCdnEnabled) { // - // even if the change is not refresh all, we still need to reset stale version. + // even if the change is not refresh all, we still need to reset stale config version. _configVersion = null; } } @@ -1426,15 +1417,15 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa return changedEtag; } - private static string CalculateHash(IEnumerable etags) + private static string CalculateCacheConsistencyToken(IEnumerable etags) { - Debug.Assert(etags != null && etags.Any()); + Debug.Assert(etags != null); StringBuilder inputBuilder = new StringBuilder(); // // purpose - inputBuilder.Append("etags\n"); + inputBuilder.Append("CacheConsistency\n"); foreach (string etag in etags) { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 23db4631..226ddea1 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -61,7 +61,7 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) if (options.IsCdnEnabled) { - provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints), options, _optional); + provider = new AzureAppConfigurationProvider(new Cdn.ConfigurationClientManager(clientFactory, endpoints), options, _optional); } else { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyPolicy.cs similarity index 68% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyPolicy.cs index 394fd845..e62947c8 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingPolicy.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyPolicy.cs @@ -7,32 +7,32 @@ using System.Diagnostics; using System.Web; -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { /// - /// HTTP pipeline policy that injects token into the query string for CDN cache busting. + /// HTTP pipeline policy that injects consistency token into the query string for CDN cache consistency. /// - internal class CdnCacheBustingPolicy : HttpPipelinePolicy + internal class CacheConsistencyPolicy : HttpPipelinePolicy { - private readonly ICdnCacheBustingAccessor _cacheBustingAccessor; + private readonly ICacheConsistencyTokenAccessor _cacheConsistencyAccessor; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The CDN cache busting accessor. - public CdnCacheBustingPolicy(ICdnCacheBustingAccessor cacheBustingAccessor) + /// The CDN cache consistency accessor. + public CacheConsistencyPolicy(ICacheConsistencyTokenAccessor cacheConsistencyAccessor) { - _cacheBustingAccessor = cacheBustingAccessor ?? throw new ArgumentNullException(nameof(cacheBustingAccessor)); + _cacheConsistencyAccessor = cacheConsistencyAccessor ?? throw new ArgumentNullException(nameof(cacheConsistencyAccessor)); } /// - /// Processes the HTTP message and injects token into query string if CDN cache busting is enabled. + /// Processes the HTTP message and injects token into query string if CDN cache consistency is enabled. /// /// The HTTP message. /// The pipeline. public override void Process(HttpMessage message, ReadOnlyMemory pipeline) { - string token = _cacheBustingAccessor.CurrentToken; + string token = _cacheConsistencyAccessor.Current; if (!string.IsNullOrEmpty(token)) { message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); @@ -42,14 +42,14 @@ public override void Process(HttpMessage message, ReadOnlyMemory - /// Processes the HTTP message asynchronously and injects token into query string if CDN cache busting is enabled. + /// Processes the HTTP message asynchronously and injects token into query string if CDN cache consistency is enabled. /// /// The HTTP message. /// The pipeline. /// A task representing the asynchronous operation. public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) { - string token = _cacheBustingAccessor.CurrentToken; + string token = _cacheConsistencyAccessor.Current; if (!string.IsNullOrEmpty(token)) { message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyTokenAccessor.cs similarity index 60% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingAccessor.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyTokenAccessor.cs index 5664d823..24b09263 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnCacheBustingAccessor.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyTokenAccessor.cs @@ -1,20 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { /// - /// Implementation of ICdnCacheBustingAccessor that manages the current token for cache busting. + /// Implementation of ICacheConsistencyTokenAccessor that manages the current token for cache consistency. /// - internal class CdnCacheBustingAccessor : ICdnCacheBustingAccessor + internal class CacheConsistencyTokenAccessor : ICacheConsistencyTokenAccessor { private string _currentToken; /// - /// Gets or sets the current token value to be used for cache busting. - /// When null, CDN cache busting is disabled. When not null, the token will be injected into requests. + /// Gets or sets the current token value to be used for cache consistency. + /// When null, cache consistency is disabled. When not null, the token will be injected into requests. /// - public string CurrentToken + public string Current { get => _currentToken; set => _currentToken = value; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs similarity index 94% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs index 94921972..8936a626 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs @@ -6,13 +6,13 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { - internal class CdnConfigurationClientManager : IConfigurationClientManager + internal class ConfigurationClientManager : IConfigurationClientManager { private readonly IList _clients; - public CdnConfigurationClientManager( + public ConfigurationClientManager( IAzureClientFactory clientFactory, IEnumerable endpoints) { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs index e0167cde..4ea6dba3 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { /// /// A token credential that provides an empty token. diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnCacheBustingAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICacheConsistencyTokenAccessor.cs similarity index 50% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnCacheBustingAccessor.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICacheConsistencyTokenAccessor.cs index cd24323f..d68c7a5a 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnCacheBustingAccessor.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICacheConsistencyTokenAccessor.cs @@ -1,17 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { - /// - /// Provides access to CDN cache busting context for managing token injection into HTTP requests. - /// - internal interface ICdnCacheBustingAccessor + // + // Interface for accessing the cache consistency token used when connecting to a CDN. + // + internal interface ICacheConsistencyTokenAccessor { /// - /// Gets or sets the current token value to be used for cache busting. - /// When null, CDN cache busting is disabled. When not null, the token will be injected into requests. + /// Gets or sets the current token value to be used for cache consistency. + /// When null, cache consistency is disabled. When not null, the token will be injected into requests. /// - string CurrentToken { get; set; } + string Current { get; set; } } } From 43a83a01fb208b4c14426babf14c7cf446a18ddd Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 3 Jun 2025 15:41:59 -0700 Subject: [PATCH 26/65] nit: use nameof on exception message string --- .../AzureAppConfigurationOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 0b62fafb..458b6c72 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -380,7 +380,7 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) { if (IsCdnEnabled) { - throw new InvalidOperationException("Please call ConnectCdn() only once."); + throw new InvalidOperationException($"Please call {nameof(AzureAppConfigurationOptions.ConnectCdn)} only once."); } if ((Credential != null) || (ConnectionStrings?.Any() ?? false)) From 7ff8e94c9978f9249a581b0f7e23201cefb624ba Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 3 Jun 2025 15:53:45 -0700 Subject: [PATCH 27/65] nit: Cdn.ConfigurationClientManager takes one cdn endpoint --- .../AzureAppConfigurationSource.cs | 2 +- .../Cdn/ConfigurationClientManager.cs | 26 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 226ddea1..005c6fd3 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -61,7 +61,7 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) if (options.IsCdnEnabled) { - provider = new AzureAppConfigurationProvider(new Cdn.ConfigurationClientManager(clientFactory, endpoints), options, _optional); + provider = new AzureAppConfigurationProvider(new Cdn.ConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); } else { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs index 8936a626..43703a5d 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs @@ -10,25 +10,28 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { internal class ConfigurationClientManager : IConfigurationClientManager { - private readonly IList _clients; + private readonly ConfigurationClientWrapper _client; public ConfigurationClientManager( IAzureClientFactory clientFactory, - IEnumerable endpoints) + Uri endpoint) { if (clientFactory == null) { throw new ArgumentNullException(nameof(clientFactory)); } - _clients = endpoints - .Select(endpoint => new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri))) - .ToList(); + if (endpoint == null) + { + throw new ArgumentNullException(nameof(endpoint)); + } + + _client = new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri)); } public IEnumerable GetClients() { - return _clients.Select(c => c.Client); + return new[] { _client.Client }; } public void RefreshClients() @@ -48,12 +51,9 @@ public bool UpdateSyncToken(Uri endpoint, string syncToken) throw new ArgumentNullException(nameof(syncToken)); } - ConfigurationClientWrapper clientWrapper = _clients.SingleOrDefault(c => new EndpointComparer().Equals(c.Endpoint, endpoint)); - - if (clientWrapper != null) + if (new EndpointComparer().Equals(_client.Endpoint, endpoint)) { - clientWrapper.Client.UpdateSyncToken(syncToken); - + _client.Client.UpdateSyncToken(syncToken); return true; } @@ -67,9 +67,7 @@ public Uri GetEndpointForClient(ConfigurationClient client) throw new ArgumentNullException(nameof(client)); } - ConfigurationClientWrapper currentClient = _clients.FirstOrDefault(c => c.Client == client); - - return currentClient?.Endpoint; + return _client.Client == client ? _client.Endpoint : null; } } } From 73be7e9635b4ef50334c775d18d0ecbdafc89468 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 3 Jun 2025 15:55:14 -0700 Subject: [PATCH 28/65] nit: _client -> _clientWrapper --- .../Cdn/ConfigurationClientManager.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs index 43703a5d..d4a060f8 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { internal class ConfigurationClientManager : IConfigurationClientManager { - private readonly ConfigurationClientWrapper _client; + private readonly ConfigurationClientWrapper _clientWrapper; public ConfigurationClientManager( IAzureClientFactory clientFactory, @@ -26,12 +26,12 @@ public ConfigurationClientManager( throw new ArgumentNullException(nameof(endpoint)); } - _client = new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri)); + _clientWrapper = new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri)); } public IEnumerable GetClients() { - return new[] { _client.Client }; + return new[] { _clientWrapper.Client }; } public void RefreshClients() @@ -51,9 +51,9 @@ public bool UpdateSyncToken(Uri endpoint, string syncToken) throw new ArgumentNullException(nameof(syncToken)); } - if (new EndpointComparer().Equals(_client.Endpoint, endpoint)) + if (new EndpointComparer().Equals(_clientWrapper.Endpoint, endpoint)) { - _client.Client.UpdateSyncToken(syncToken); + _clientWrapper.Client.UpdateSyncToken(syncToken); return true; } @@ -67,7 +67,7 @@ public Uri GetEndpointForClient(ConfigurationClient client) throw new ArgumentNullException(nameof(client)); } - return _client.Client == client ? _client.Endpoint : null; + return _clientWrapper.Client == client ? _clientWrapper.Endpoint : null; } } } From bd941f08ddb3f260501da4fb9507c5bfab51fe5b Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 3 Jun 2025 18:01:40 -0700 Subject: [PATCH 29/65] remove last newline, sort etags before --- .../AzureAppConfigurationProvider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index b8f6825c..c3b14fcd 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1427,12 +1427,14 @@ private static string CalculateCacheConsistencyToken(IEnumerable etags) // purpose inputBuilder.Append("CacheConsistency\n"); - foreach (string etag in etags) + foreach (string etag in etags.OrderBy(etag => etag, StringComparer.Ordinal)) { inputBuilder.Append(etag); inputBuilder.Append('\n'); } + inputBuilder.Length--; // Remove the last newline character + using SHA256 sha256 = SHA256.Create(); return sha256.ComputeHash(Encoding.UTF8.GetBytes(inputBuilder.ToString())).ToBase64Url(); From b89f870dfdac45de66949f8a6a2c6b9e00e92abf Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 4 Jun 2025 00:48:13 -0700 Subject: [PATCH 30/65] rename --- .../AzureAppConfigurationSource.cs | 3 ++- ...ationClientManager.cs => CdnConfigurationClientManager.cs} | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{ConfigurationClientManager.cs => CdnConfigurationClientManager.cs} (94%) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 005c6fd3..b6b52700 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -3,6 +3,7 @@ // using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn; using System; using System.Collections.Generic; using System.Linq; @@ -61,7 +62,7 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) if (options.IsCdnEnabled) { - provider = new AzureAppConfigurationProvider(new Cdn.ConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); + provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); } else { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs similarity index 94% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs index d4a060f8..b2fee5fb 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs @@ -8,11 +8,11 @@ using System.Linq; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { - internal class ConfigurationClientManager : IConfigurationClientManager + internal class CdnConfigurationClientManager : IConfigurationClientManager { private readonly ConfigurationClientWrapper _clientWrapper; - public ConfigurationClientManager( + public CdnConfigurationClientManager( IAzureClientFactory clientFactory, Uri endpoint) { From 4f5108f0525e281372d09f92161f8905a83e3fa2 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 4 Jun 2025 00:49:33 -0700 Subject: [PATCH 31/65] handle deleted sentinel kv case --- .../AzureAppConfigurationProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index c3b14fcd..fff0762f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1050,7 +1050,7 @@ await CallWithRequestTracing( if (kvWatcher.RefreshAll) { - return change.Current.ETag.ToString(); + return change.Current?.ETag.ToString() ?? CalculateCacheConsistencyToken(new[] { change.Previous.ETag.ToString() });// in case of deleted sentinel key } if (_options.IsCdnEnabled) From 4fc69d3f2db5a5bfecaea4a3b7d01d90dcea6c74 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 4 Jun 2025 16:06:33 -0700 Subject: [PATCH 32/65] redesign, no need to ensure state does not regress, eventual consistency will be eventually achieved. --- .../AzureAppConfigurationProvider.cs | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index fff0762f..411992dc 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -313,7 +313,6 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_options.IsCdnEnabled) { - _configVersion ??= CalculateCacheConsistencyToken(_kvEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); _options.CacheConsistencyTokenAccessor.Current = _configVersion; } @@ -328,7 +327,6 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_options.IsCdnEnabled) { - _configVersion ??= CalculateCacheConsistencyToken(_watchedIndividualKvs.Select(kvp => kvp.Value.ETag.ToString())); _options.CacheConsistencyTokenAccessor.Current = _configVersion; } @@ -358,8 +356,8 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => // // Reset versions so that next watch request will not use stale versions. - _configVersion = null; - _ffCollectionVersion = null; + _configVersion = refreshAllChangedEtag; + _ffCollectionVersion = refreshAllChangedEtag; } data = await LoadSelected(client, kvEtags, ffEtags, _options.Selectors, ffKeys, cancellationToken).ConfigureAwait(false); @@ -371,7 +369,6 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => // Get feature flag changes if (_options.IsCdnEnabled) { - _ffCollectionVersion ??= CalculateCacheConsistencyToken(_ffEtags.SelectMany(kvp => kvp.Value.Select(mc => mc.IfNoneMatch.ToString()))); _options.CacheConsistencyTokenAccessor.Current = _ffCollectionVersion; } @@ -399,7 +396,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => // // Reset ff collection version so that next ff watch request will not use stale version. - _ffCollectionVersion = null; + _ffCollectionVersion = ffCollectionUpdatedChangedEtag; } ffCollectionData = await LoadSelected( @@ -1050,14 +1047,17 @@ await CallWithRequestTracing( if (kvWatcher.RefreshAll) { - return change.Current?.ETag.ToString() ?? CalculateCacheConsistencyToken(new[] { change.Previous.ETag.ToString() });// in case of deleted sentinel key - } + string changedEtag = change.Current?.ETag.ToString(); - if (_options.IsCdnEnabled) - { // - // even if the change is not refresh all, we still need to reset stale config version. - _configVersion = null; + // falback in case of deleted sentinel key-value + if (changedEtag == null) + { + using SHA256 sha256 = SHA256.Create(); + changedEtag = sha256.ComputeHash(Encoding.UTF8.GetBytes($"ResourceDeleted\n{change.Previous.ETag}")).ToBase64Url(); + } + + return changedEtag; } } else @@ -1417,29 +1417,6 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa return changedEtag; } - private static string CalculateCacheConsistencyToken(IEnumerable etags) - { - Debug.Assert(etags != null); - - StringBuilder inputBuilder = new StringBuilder(); - - // - // purpose - inputBuilder.Append("CacheConsistency\n"); - - foreach (string etag in etags.OrderBy(etag => etag, StringComparer.Ordinal)) - { - inputBuilder.Append(etag); - inputBuilder.Append('\n'); - } - - inputBuilder.Length--; // Remove the last newline character - - using SHA256 sha256 = SHA256.Create(); - - return sha256.ComputeHash(Encoding.UTF8.GetBytes(inputBuilder.ToString())).ToBase64Url(); - } - private async Task ProcessKeyValueChangesAsync( IEnumerable keyValueChanges, Dictionary mappedData, From 1d621857be37e5bb5660267935d961120fb5ee03 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 4 Jun 2025 16:16:15 -0700 Subject: [PATCH 33/65] disable load balancing and replica discovery for cdn scenario --- .../AzureAppConfigurationOptions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 458b6c72..7fccad64 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -395,6 +395,11 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); + // + // We do not perform replica discovery and load balancing for CDN. + ReplicaDiscoveryEnabled = false; + LoadBalancingEnabled = false; + CacheConsistencyTokenAccessor = new CacheConsistencyTokenAccessor(); ClientOptions.AddPolicy(new CacheConsistencyPolicy(CacheConsistencyTokenAccessor), HttpPipelinePosition.PerCall); From 6528a366fe7e85f644e21c88347bf23f29e713aa Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 5 Jun 2025 14:13:55 -0700 Subject: [PATCH 34/65] nit: rename cdn classes accordingly --- .../AzureAppConfigurationOptions.cs | 11 ++++---- .../AzureAppConfigurationProvider.cs | 10 +++---- ...CacheConsistencyPolicy.cs => CdnPolicy.cs} | 27 +++++++++++-------- ...cyTokenAccessor.cs => CdnTokenAccessor.cs} | 8 +++--- ...yTokenAccessor.cs => ICdnTokenAccessor.cs} | 8 +++--- 5 files changed, 34 insertions(+), 30 deletions(-) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{CacheConsistencyPolicy.cs => CdnPolicy.cs} (55%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{CacheConsistencyTokenAccessor.cs => CdnTokenAccessor.cs} (59%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/{ICacheConsistencyTokenAccessor.cs => ICdnTokenAccessor.cs} (55%) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 7fccad64..cdf4cd68 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -158,15 +158,14 @@ internal IEnumerable Adapters internal IAzureClientFactory ClientFactory { get; private set; } /// - /// Accessor for CDN cache busting context that manages ETag injection into requests. - /// When null, CDN cache consistency is disabled. When not null, CDN cache consistency is enabled. + /// An accessor for current token to be used for CDN cache breakage/consistency. /// - internal ICacheConsistencyTokenAccessor CacheConsistencyTokenAccessor { get; private set; } + internal ICdnTokenAccessor CdnTokenAccessor { get; private set; } /// /// Gets a value indicating whether CDN is enabled. /// - internal bool IsCdnEnabled => CacheConsistencyTokenAccessor != null; + internal bool IsCdnEnabled => CdnTokenAccessor != null; /// /// Initializes a new instance of the class. @@ -400,8 +399,8 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) ReplicaDiscoveryEnabled = false; LoadBalancingEnabled = false; - CacheConsistencyTokenAccessor = new CacheConsistencyTokenAccessor(); - ClientOptions.AddPolicy(new CacheConsistencyPolicy(CacheConsistencyTokenAccessor), HttpPipelinePosition.PerCall); + CdnTokenAccessor = new CdnTokenAccessor(); + ClientOptions.AddPolicy(new CdnPolicy(CdnTokenAccessor), HttpPipelinePosition.PerCall); return result; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 411992dc..08f108a2 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -313,7 +313,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_options.IsCdnEnabled) { - _options.CacheConsistencyTokenAccessor.Current = _configVersion; + _options.CdnTokenAccessor.Current = _configVersion; } refreshAllChangedEtag = await HaveCollectionsChanged( @@ -327,7 +327,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { if (_options.IsCdnEnabled) { - _options.CacheConsistencyTokenAccessor.Current = _configVersion; + _options.CdnTokenAccessor.Current = _configVersion; } refreshAllChangedEtag = await RefreshIndividualKvWatchers( @@ -352,7 +352,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { // // Break cdn cache - _options.CacheConsistencyTokenAccessor.Current = refreshAllChangedEtag; + _options.CdnTokenAccessor.Current = refreshAllChangedEtag; // // Reset versions so that next watch request will not use stale versions. @@ -369,7 +369,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => // Get feature flag changes if (_options.IsCdnEnabled) { - _options.CacheConsistencyTokenAccessor.Current = _ffCollectionVersion; + _options.CdnTokenAccessor.Current = _ffCollectionVersion; } ffCollectionUpdatedChangedEtag = await HaveCollectionsChanged( @@ -392,7 +392,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => { // // Break cdn cache - _options.CacheConsistencyTokenAccessor.Current = ffCollectionUpdatedChangedEtag; + _options.CdnTokenAccessor.Current = ffCollectionUpdatedChangedEtag; // // Reset ff collection version so that next ff watch request will not use stale version. diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs similarity index 55% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyPolicy.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs index e62947c8..2b21650b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyPolicy.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs @@ -10,29 +10,32 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { /// - /// HTTP pipeline policy that injects consistency token into the query string for CDN cache consistency. + /// HTTP pipeline policy that injects current token into the query string for CDN cache breakage and consistency. + /// The injected token ensures CDN cache invalidation when configuration changes are detected and maintaining eventual consistency across distributed instances. /// - internal class CacheConsistencyPolicy : HttpPipelinePolicy + internal class CdnPolicy : HttpPipelinePolicy { - private readonly ICacheConsistencyTokenAccessor _cacheConsistencyAccessor; + private readonly ICdnTokenAccessor _cdnTokenAccessor; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The CDN cache consistency accessor. - public CacheConsistencyPolicy(ICacheConsistencyTokenAccessor cacheConsistencyAccessor) + /// The token accessor that provides current token to be used for CDN cache breakage/consistency. + public CdnPolicy(ICdnTokenAccessor cdnTokenAccessor) { - _cacheConsistencyAccessor = cacheConsistencyAccessor ?? throw new ArgumentNullException(nameof(cacheConsistencyAccessor)); + _cdnTokenAccessor = cdnTokenAccessor ?? throw new ArgumentNullException(nameof(cdnTokenAccessor)); } /// - /// Processes the HTTP message and injects token into query string if CDN cache consistency is enabled. + /// Processes the HTTP message and injects token into query string to break CDN cache when changes are detected. + /// This ensures fresh configuration data is retrieved when sentinel keys or collections have been modified. + /// It also maintains eventual consistency across distributed instances by ensuring that the same token is used for all subsequent watch requests, until a new change is detected. /// /// The HTTP message. /// The pipeline. public override void Process(HttpMessage message, ReadOnlyMemory pipeline) { - string token = _cacheConsistencyAccessor.Current; + string token = _cdnTokenAccessor.Current; if (!string.IsNullOrEmpty(token)) { message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); @@ -42,14 +45,16 @@ public override void Process(HttpMessage message, ReadOnlyMemory - /// Processes the HTTP message asynchronously and injects token into query string if CDN cache consistency is enabled. + /// Processes the HTTP message asynchronously and injects token into query string to break CDN cache when changes are detected. + /// This ensures fresh configuration data is retrieved when sentinel keys or collections have been modified. + /// It also maintains eventual consistency across distributed instances by ensuring that the same token is used for all subsequent watch requests, until a new change is detected. /// /// The HTTP message. /// The pipeline. /// A task representing the asynchronous operation. public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) { - string token = _cacheConsistencyAccessor.Current; + string token = _cdnTokenAccessor.Current; if (!string.IsNullOrEmpty(token)) { message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyTokenAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnTokenAccessor.cs similarity index 59% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyTokenAccessor.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnTokenAccessor.cs index 24b09263..f4d30abc 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CacheConsistencyTokenAccessor.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnTokenAccessor.cs @@ -4,15 +4,15 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { /// - /// Implementation of ICacheConsistencyTokenAccessor that manages the current token for cache consistency. + /// Implementation of ICdnTokenAccessor that manages the current token for CDN cache breakage/consistency. /// - internal class CacheConsistencyTokenAccessor : ICacheConsistencyTokenAccessor + internal class CdnTokenAccessor : ICdnTokenAccessor { private string _currentToken; /// - /// Gets or sets the current token value to be used for cache consistency. - /// When null, cache consistency is disabled. When not null, the token will be injected into requests. + /// Gets or sets the current token value to be used for CDN cache breakage/consistency. + /// When null, CDN cache breakage/consistency is disabled. When not null, the token will be injected into requests. /// public string Current { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICacheConsistencyTokenAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnTokenAccessor.cs similarity index 55% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICacheConsistencyTokenAccessor.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnTokenAccessor.cs index d68c7a5a..e78fd7cb 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICacheConsistencyTokenAccessor.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnTokenAccessor.cs @@ -4,13 +4,13 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { // - // Interface for accessing the cache consistency token used when connecting to a CDN. + // Interface for accessing the CDN cache breakage/consistency token. // - internal interface ICacheConsistencyTokenAccessor + internal interface ICdnTokenAccessor { /// - /// Gets or sets the current token value to be used for cache consistency. - /// When null, cache consistency is disabled. When not null, the token will be injected into requests. + /// Gets or sets the current token value to be used for CDN cache breakage/consistency. + /// When null, CDN cache breakage/consistency is disabled. When not null, the token will be injected into requests. /// string Current { get; set; } } From a846f0f5e25445ded03d59987fe2ed1e1dbd0779 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 5 Jun 2025 14:47:32 -0700 Subject: [PATCH 35/65] nit: change HaveCollectionChanged to reflect its new role. --- .../AzureAppConfigurationProvider.cs | 8 ++++---- .../Extensions/ConfigurationClientExtensions.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 08f108a2..9fb9bc0d 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -316,7 +316,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => _options.CdnTokenAccessor.Current = _configVersion; } - refreshAllChangedEtag = await HaveCollectionsChanged( + refreshAllChangedEtag = await GetCollectionChangeEtag( _options.Selectors.Where(selector => !selector.IsFeatureFlagSelector), _kvEtags, client, @@ -372,7 +372,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => _options.CdnTokenAccessor.Current = _ffCollectionVersion; } - ffCollectionUpdatedChangedEtag = await HaveCollectionsChanged( + ffCollectionUpdatedChangedEtag = await GetCollectionChangeEtag( refreshableFfWatchers.Select(watcher => new KeyValueSelector { KeyFilter = watcher.Key, @@ -1386,7 +1386,7 @@ private void UpdateClientBackoffStatus(Uri endpoint, bool successful) _configClientBackoffs[endpoint] = clientBackoffStatus; } - private async Task HaveCollectionsChanged( + private async Task GetCollectionChangeEtag( IEnumerable selectors, Dictionary> pageEtags, ConfigurationClient client, @@ -1399,7 +1399,7 @@ private async Task HaveCollectionsChanged( if (pageEtags.TryGetValue(selector, out IEnumerable matchConditions)) { await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => changedEtag = await client.HaveCollectionsChanged( + async () => changedEtag = await client.GetCollectionChangeEtag( selector, matchConditions, _options.ConfigurationSettingPageIterator, diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index c51cf57a..4e036bb5 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -64,7 +64,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli }; } - public static async Task HaveCollectionsChanged(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, bool makeConditionalRequest, CancellationToken cancellationToken) + public static async Task GetCollectionChangeEtag(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, bool makeConditionalRequest, CancellationToken cancellationToken) { if (matchConditions == null) { From c95e9ced6825f2d5e7ae5cff997258eb8ff0075f Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 5 Jun 2025 14:52:18 -0700 Subject: [PATCH 36/65] nit --- .../AzureAppConfigurationProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 9fb9bc0d..d9c2b872 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -316,7 +316,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => _options.CdnTokenAccessor.Current = _configVersion; } - refreshAllChangedEtag = await GetCollectionChangeEtag( + refreshAllChangedEtag = await GetCollectionsChangeEtag( _options.Selectors.Where(selector => !selector.IsFeatureFlagSelector), _kvEtags, client, @@ -372,7 +372,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => _options.CdnTokenAccessor.Current = _ffCollectionVersion; } - ffCollectionUpdatedChangedEtag = await GetCollectionChangeEtag( + ffCollectionUpdatedChangedEtag = await GetCollectionsChangeEtag( refreshableFfWatchers.Select(watcher => new KeyValueSelector { KeyFilter = watcher.Key, @@ -1386,7 +1386,7 @@ private void UpdateClientBackoffStatus(Uri endpoint, bool successful) _configClientBackoffs[endpoint] = clientBackoffStatus; } - private async Task GetCollectionChangeEtag( + private async Task GetCollectionsChangeEtag( IEnumerable selectors, Dictionary> pageEtags, ConfigurationClient client, From e1740c94fb98ec32260c9b7ded9deca2600f5d7c Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 5 Jun 2025 15:00:05 -0700 Subject: [PATCH 37/65] load balancing is not supported when cdn enabled --- .../AzureAppConfigurationOptions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index cdf4cd68..7d13eced 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -392,12 +392,12 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) throw new ArgumentNullException(nameof(endpoint)); } - var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); + if (LoadBalancingEnabled) + { + throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); + } - // - // We do not perform replica discovery and load balancing for CDN. - ReplicaDiscoveryEnabled = false; - LoadBalancingEnabled = false; + var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); CdnTokenAccessor = new CdnTokenAccessor(); ClientOptions.AddPolicy(new CdnPolicy(CdnTokenAccessor), HttpPipelinePosition.PerCall); From 29eed77c6b6f99b8ba75281ac43f0d6db5878ace Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 5 Jun 2025 18:11:10 -0700 Subject: [PATCH 38/65] move load balacing check when cdn is enabled to source --- .../AzureAppConfigurationOptions.cs | 5 ----- .../AzureAppConfigurationSource.cs | 5 +++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 7d13eced..2bee9263 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -392,11 +392,6 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) throw new ArgumentNullException(nameof(endpoint)); } - if (LoadBalancingEnabled) - { - throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); - } - var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); CdnTokenAccessor = new CdnTokenAccessor(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index b6b52700..08e37c79 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -62,6 +62,11 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) if (options.IsCdnEnabled) { + if (options.LoadBalancingEnabled) + { + throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); + } + provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); } else From 66eccf6f206b98662283240d8a44b78543952baf Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 5 Jun 2025 18:15:36 -0700 Subject: [PATCH 39/65] be clearer --- .../AzureAppConfigurationProvider.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index d9c2b872..92560d95 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -1047,15 +1047,17 @@ await CallWithRequestTracing( if (kvWatcher.RefreshAll) { - string changedEtag = change.Current?.ETag.ToString(); + string changedEtag; - // - // falback in case of deleted sentinel key-value - if (changedEtag == null) + if (change.ChangeType == KeyValueChangeType.Deleted) { using SHA256 sha256 = SHA256.Create(); changedEtag = sha256.ComputeHash(Encoding.UTF8.GetBytes($"ResourceDeleted\n{change.Previous.ETag}")).ToBase64Url(); } + else + { + changedEtag = change.Current.ETag.ToString(); + } return changedEtag; } From 326d85b3c13b44394c3c2a7614f4c0081374c5b1 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 15:43:27 -0700 Subject: [PATCH 40/65] address avani's comments --- .../AzureAppConfigurationOptions.cs | 12 +++--------- .../AzureAppConfigurationSource.cs | 4 ++++ .../Cdn/CdnConfigurationClientManager.cs | 1 - 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 2bee9263..aa0079c3 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -160,12 +160,12 @@ internal IEnumerable Adapters /// /// An accessor for current token to be used for CDN cache breakage/consistency. /// - internal ICdnTokenAccessor CdnTokenAccessor { get; private set; } + internal ICdnTokenAccessor CdnTokenAccessor { get; set; } /// /// Gets a value indicating whether CDN is enabled. /// - internal bool IsCdnEnabled => CdnTokenAccessor != null; + internal bool IsCdnEnabled { get; private set; } = false; /// /// Initializes a new instance of the class. @@ -377,11 +377,6 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// The endpoint of the CDN instance to connect to. public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) { - if (IsCdnEnabled) - { - throw new InvalidOperationException($"Please call {nameof(AzureAppConfigurationOptions.ConnectCdn)} only once."); - } - if ((Credential != null) || (ConnectionStrings?.Any() ?? false)) { throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); @@ -394,8 +389,7 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); - CdnTokenAccessor = new CdnTokenAccessor(); - ClientOptions.AddPolicy(new CdnPolicy(CdnTokenAccessor), HttpPipelinePosition.PerCall); + IsCdnEnabled = true; return result; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 08e37c79..aebdec6c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // +using Azure.Core; using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn; @@ -67,6 +68,9 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); } + options.CdnTokenAccessor = new CdnTokenAccessor(); + options.ClientOptions.AddPolicy(new CdnPolicy(options.CdnTokenAccessor), HttpPipelinePosition.PerCall); + provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); } else diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs index b2fee5fb..3cebe84a 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Azure; using System; using System.Collections.Generic; -using System.Linq; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn { internal class CdnConfigurationClientManager : IConfigurationClientManager From 984650e2cd5556ae54c0abcef8eaf7d181f78ec2 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 15:59:47 -0700 Subject: [PATCH 41/65] adopt new pattern for tracing features --- .../RequestTracingOptions.cs | 13 ++++++++++++- .../TracingUtils.cs | 5 ----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs index 85f1ab1c..819f8397 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs @@ -125,7 +125,8 @@ public bool UsesAnyTracingFeature() return IsLoadBalancingEnabled || IsSignalRUsed || UsesAIConfiguration || - UsesAIChatCompletionConfiguration; + UsesAIChatCompletionConfiguration || + IsCdnEnabled; } /// @@ -176,6 +177,16 @@ public string CreateFeaturesString() sb.Append(RequestTracingConstants.AIChatCompletionConfigurationTag); } + if (IsCdnEnabled) + { + if (sb.Length > 0) + { + sb.Append(RequestTracingConstants.Delimiter); + } + + sb.Append(RequestTracingConstants.CdnTag); + } + return sb.ToString(); } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs index 99a6604d..b3e12913 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/TracingUtils.cs @@ -206,11 +206,6 @@ private static string CreateCorrelationContextHeader(RequestType requestType, Re correlationContextTags.Add(RequestTracingConstants.PushRefreshTag); } - if (requestTracingOptions.IsCdnEnabled) - { - correlationContextTags.Add(RequestTracingConstants.CdnTag); - } - var sb = new StringBuilder(); foreach (KeyValuePair kvp in correlationContextKeyValues) From e64307a51d26c241b0081318d1c5679232a4283e Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 16:03:32 -0700 Subject: [PATCH 42/65] bug fix --- .../AzureAppConfigurationOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index aa0079c3..cedf832e 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -377,7 +377,7 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// The endpoint of the CDN instance to connect to. public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) { - if ((Credential != null) || (ConnectionStrings?.Any() ?? false)) + if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) { throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); } From f19cb5de9fc50b1439dfde848e3d11981b1cce90 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 17:01:04 -0700 Subject: [PATCH 43/65] address jimmy's refactor comment --- .../AzureAppConfigurationProvider.cs | 307 +++++++++--------- .../ConfigurationClientExtensions.cs | 7 +- .../Extensions/PageExtensions.cs | 17 + .../KeyValueChange.cs | 20 ++ 4 files changed, 188 insertions(+), 163 deletions(-) create mode 100644 src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 92560d95..1a78fb25 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -286,8 +286,8 @@ public async Task RefreshAsync(CancellationToken cancellationToken) List keyValueChanges = null; Dictionary data = null; Dictionary ffCollectionData = null; - string ffCollectionUpdatedChangedEtag = null; - string refreshAllChangedEtag = null; + bool ffCollectionUpdated = false; + bool refreshAll = false; StringBuilder logInfoBuilder = new StringBuilder(); StringBuilder logDebugBuilder = new StringBuilder(); @@ -300,47 +300,103 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => keyValueChanges = new List(); data = null; ffCollectionData = null; - ffCollectionUpdatedChangedEtag = null; - refreshAllChangedEtag = null; + ffCollectionUpdated = false; + refreshAll = false; logDebugBuilder.Clear(); logInfoBuilder.Clear(); Uri endpoint = _configClientManager.GetEndpointForClient(client); + if (_options.IsCdnEnabled) + { + _options.CdnTokenAccessor.Current = _configVersion; + } + if (_options.RegisterAllEnabled) { // Get key value collection changes if RegisterAll was called if (isRefreshDue) { - if (_options.IsCdnEnabled) + foreach (KeyValueSelector selector in _options.Selectors.Where(selector => !selector.IsFeatureFlagSelector)) { - _options.CdnTokenAccessor.Current = _configVersion; + Page changedPage = null; + + if (_kvEtags.TryGetValue(selector, out IEnumerable matchConditions)) + { + await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, + async () => changedPage = await client.GetPageChange( + selector, + matchConditions, + _options.ConfigurationSettingPageIterator, + makeConditionalRequest: !_options.IsCdnEnabled, + cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + } + + if (changedPage != null) + { + refreshAll = true; + + if (_options.IsCdnEnabled) + { + // + // Break cdn cache + string token = changedPage.GetCdnToken(); + _options.CdnTokenAccessor.Current = token; + + // + // Reset versions so that next watch request will not use stale versions. + _configVersion = token; + _ffCollectionVersion = token; + } + + break; + } } - - refreshAllChangedEtag = await GetCollectionsChangeEtag( - _options.Selectors.Where(selector => !selector.IsFeatureFlagSelector), - _kvEtags, - client, - cancellationToken).ConfigureAwait(false); } } else { - if (_options.IsCdnEnabled) + foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers) { - _options.CdnTokenAccessor.Current = _configVersion; - } + KeyValueChange change = await CheckForChange(client, kvWatcher, cancellationToken).ConfigureAwait(false); - refreshAllChangedEtag = await RefreshIndividualKvWatchers( - client, - keyValueChanges, - refreshableIndividualKvWatchers, - endpoint, - logDebugBuilder, - logInfoBuilder, - cancellationToken).ConfigureAwait(false); + // + // Skip if no change detected + if (change.ChangeType == KeyValueChangeType.None) + { + logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString())); + + continue; + } + + logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString())); + + logInfoBuilder.AppendLine(LogHelper.BuildKeyValueSettingUpdatedMessage(change.Key)); + + keyValueChanges.Add(change); + + if (kvWatcher.RefreshAll) + { + refreshAll = true; + + if (_options.IsCdnEnabled) + { + // + // Break cdn cache + string token = change.GetCdnToken(); + _options.CdnTokenAccessor.Current = token; + + // + // Reset versions so that next watch request will not use stale versions. + _configVersion = token; + _ffCollectionVersion = token; + } + + break; + } + } } - if (refreshAllChangedEtag != null) + if (refreshAll) { // Trigger a single load-all operation if a change was detected in one or more key-values with refreshAll: true, // or if any key-value collection change was detected. @@ -348,18 +404,6 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => ffEtags = new Dictionary>(); ffKeys = new HashSet(); - if (_options.IsCdnEnabled) - { - // - // Break cdn cache - _options.CdnTokenAccessor.Current = refreshAllChangedEtag; - - // - // Reset versions so that next watch request will not use stale versions. - _configVersion = refreshAllChangedEtag; - _ffCollectionVersion = refreshAllChangedEtag; - } - data = await LoadSelected(client, kvEtags, ffEtags, _options.Selectors, ffKeys, cancellationToken).ConfigureAwait(false); watchedIndividualKvs = await LoadKeyValuesRegisteredForRefresh(client, data, cancellationToken).ConfigureAwait(false); logInfoBuilder.AppendLine(LogHelper.BuildConfigurationUpdatedMessage()); @@ -372,32 +416,52 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => _options.CdnTokenAccessor.Current = _ffCollectionVersion; } - ffCollectionUpdatedChangedEtag = await GetCollectionsChangeEtag( - refreshableFfWatchers.Select(watcher => new KeyValueSelector - { - KeyFilter = watcher.Key, - LabelFilter = watcher.Label, - IsFeatureFlagSelector = true - }), - _ffEtags, - client, - cancellationToken).ConfigureAwait(false); - - if (ffCollectionUpdatedChangedEtag != null) + var ffSelectors = refreshableFfWatchers.Select(watcher => new KeyValueSelector { - ffEtags = new Dictionary>(); - ffKeys = new HashSet(); + KeyFilter = watcher.Key, + LabelFilter = watcher.Label, + IsFeatureFlagSelector = true + }); - if (_options.IsCdnEnabled) + foreach (KeyValueSelector selector in ffSelectors) + { + Page changedPage = null; + + if (_ffEtags.TryGetValue(selector, out IEnumerable matchConditions)) { - // - // Break cdn cache - _options.CdnTokenAccessor.Current = ffCollectionUpdatedChangedEtag; + await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, + async () => changedPage = await client.GetPageChange( + selector, + matchConditions, + _options.ConfigurationSettingPageIterator, + makeConditionalRequest: !_options.IsCdnEnabled, + cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + } - // - // Reset ff collection version so that next ff watch request will not use stale version. - _ffCollectionVersion = ffCollectionUpdatedChangedEtag; + if (changedPage != null) + { + ffCollectionUpdated = true; + + if (_options.IsCdnEnabled) + { + // + // Break cdn cache + string token = changedPage.GetCdnToken(); + _options.CdnTokenAccessor.Current = token; + + // + // Reset ff collection version so that next ff watch request will not use stale version. + _ffCollectionVersion = token; + } + + break; } + } + + if (ffCollectionUpdated) + { + ffEtags = new Dictionary>(); + ffKeys = new HashSet(); ffCollectionData = await LoadSelected( client, @@ -417,9 +481,6 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => cancellationToken) .ConfigureAwait(false); - bool refreshAll = !string.IsNullOrEmpty(refreshAllChangedEtag); - bool ffCollectionUpdated = !string.IsNullOrEmpty(ffCollectionUpdatedChangedEtag); - if (refreshAll) { _mappedData = await MapConfigurationSettings(data).ConfigureAwait(false); @@ -987,88 +1048,47 @@ private async Task> LoadKey return watchedIndividualKvs; } - private async Task RefreshIndividualKvWatchers( - ConfigurationClient client, - List keyValueChanges, - IEnumerable refreshableIndividualKvWatchers, - Uri endpoint, - StringBuilder logDebugBuilder, - StringBuilder logInfoBuilder, - CancellationToken cancellationToken) + private async Task CheckForChange(ConfigurationClient client, KeyValueWatcher kvWatcher, CancellationToken cancellationToken) { - foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers) - { - string watchedKey = kvWatcher.Key; - string watchedLabel = kvWatcher.Label; + Debug.Assert(client != null); + Debug.Assert(kvWatcher != null); - KeyValueIdentifier watchedKeyLabel = new KeyValueIdentifier(watchedKey, watchedLabel); + KeyValueChange change = default; - KeyValueChange change = default; + // + // Find if there is a change associated with watcher + if (_watchedIndividualKvs.TryGetValue(new KeyValueIdentifier(kvWatcher.Key, kvWatcher.Label), out ConfigurationSetting watchedKv)) + { + await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, + async () => change = await client.GetKeyValueChange(watchedKv, makeConditionalRequest: !_options.IsCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + } + else + { + // Load the key-value in case the previous load attempts had failed - // - // Find if there is a change associated with watcher - if (_watchedIndividualKvs.TryGetValue(watchedKeyLabel, out ConfigurationSetting watchedKv)) + try { - await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, makeConditionalRequest: !_options.IsCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + await CallWithRequestTracing( + async () => watchedKv = await client.GetConfigurationSettingAsync(kvWatcher.Key, kvWatcher.Label, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } - else + catch (RequestFailedException e) when (e.Status == (int)HttpStatusCode.NotFound) { - // Load the key-value in case the previous load attempts had failed - - try - { - await CallWithRequestTracing( - async () => watchedKv = await client.GetConfigurationSettingAsync(watchedKey, watchedLabel, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); - } - catch (RequestFailedException e) when (e.Status == (int)HttpStatusCode.NotFound) - { - watchedKv = null; - } - - if (watchedKv != null) - { - change = new KeyValueChange() - { - Key = watchedKv.Key, - Label = watchedKv.Label.NormalizeNull(), - Current = watchedKv, - ChangeType = KeyValueChangeType.Modified - }; - } + watchedKv = null; } - // Check if a change has been detected in the key-value registered for refresh - if (change.ChangeType != KeyValueChangeType.None) + if (watchedKv != null) { - logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString())); - logInfoBuilder.AppendLine(LogHelper.BuildKeyValueSettingUpdatedMessage(change.Key)); - keyValueChanges.Add(change); - - if (kvWatcher.RefreshAll) + change = new KeyValueChange() { - string changedEtag; - - if (change.ChangeType == KeyValueChangeType.Deleted) - { - using SHA256 sha256 = SHA256.Create(); - changedEtag = sha256.ComputeHash(Encoding.UTF8.GetBytes($"ResourceDeleted\n{change.Previous.ETag}")).ToBase64Url(); - } - else - { - changedEtag = change.Current.ETag.ToString(); - } - - return changedEtag; - } - } - else - { - logDebugBuilder.AppendLine(LogHelper.BuildKeyValueReadMessage(change.ChangeType, change.Key, change.Label, endpoint.ToString())); + Key = watchedKv.Key, + Label = watchedKv.Label.NormalizeNull(), + Current = watchedKv, + ChangeType = KeyValueChangeType.Modified + }; } } - return null; + return change; } private void SetData(IDictionary data) @@ -1388,37 +1408,6 @@ private void UpdateClientBackoffStatus(Uri endpoint, bool successful) _configClientBackoffs[endpoint] = clientBackoffStatus; } - private async Task GetCollectionsChangeEtag( - IEnumerable selectors, - Dictionary> pageEtags, - ConfigurationClient client, - CancellationToken cancellationToken) - { - string changedEtag = null; - - foreach (KeyValueSelector selector in selectors) - { - if (pageEtags.TryGetValue(selector, out IEnumerable matchConditions)) - { - await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => changedEtag = await client.GetCollectionChangeEtag( - selector, - matchConditions, - _options.ConfigurationSettingPageIterator, - makeConditionalRequest: !_options.IsCdnEnabled, - cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); - } - - if (changedEtag != null) - { - // If we have a changed ETag, we can stop checking further selectors - return changedEtag; - } - } - - return changedEtag; - } - private async Task ProcessKeyValueChangesAsync( IEnumerable keyValueChanges, Dictionary mappedData, diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs index 4e036bb5..ae7d25dd 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/ConfigurationClientExtensions.cs @@ -64,7 +64,7 @@ public static async Task GetKeyValueChange(this ConfigurationCli }; } - public static async Task GetCollectionChangeEtag(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, bool makeConditionalRequest, CancellationToken cancellationToken) + public static async Task> GetPageChange(this ConfigurationClient client, KeyValueSelector keyValueSelector, IEnumerable matchConditions, IConfigurationSettingPageIterator pageIterator, bool makeConditionalRequest, CancellationToken cancellationToken) { if (matchConditions == null) { @@ -102,12 +102,11 @@ public static async Task GetCollectionChangeEtag(this ConfigurationClien !existingMatchConditionsEnumerator.Current.IfNoneMatch.Equals(response.Headers.ETag)) && response.Status == (int)HttpStatusCode.OK) { - return response.Headers.ETag.ToString(); + return page; } } - // Need to check if pages were deleted and no change was found within the new shorter list of match conditions - return existingMatchConditionsEnumerator.MoveNext() ? existingMatchConditionsEnumerator.Current.IfNoneMatch.ToString() : null; + return null; } } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs new file mode 100644 index 00000000..81f4fbf5 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs @@ -0,0 +1,17 @@ +// write one generic extension method that takes a KeyValueChange object and returns a string representation of the key and value +using Azure; +using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models; + +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions +{ + internal static class PageExtensions + { + public static string GetCdnToken(this Page page) + { + using Response response = page.GetRawResponse(); + + return response.Headers.ETag?.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs index 2286016d..3cbf1863 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. // using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions; +using System.Security.Cryptography; +using System.Text; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration { @@ -23,5 +26,22 @@ internal struct KeyValueChange public ConfigurationSetting Current { get; set; } public ConfigurationSetting Previous { get; set; } + + public string GetCdnToken() + { + string token; + + if (ChangeType == KeyValueChangeType.Deleted) + { + using SHA256 sha256 = SHA256.Create(); + token = sha256.ComputeHash(Encoding.UTF8.GetBytes($"ResourceDeleted\n{Previous.ETag}")).ToBase64Url(); + } + else + { + token = Current.ETag.ToString(); + } + + return token; + } } } From 1803e6bc39d52e78abc8faa84e998b94cd7269f2 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 17:08:48 -0700 Subject: [PATCH 44/65] change implementation --- .../AzureAppConfigurationOptions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index cedf832e..eafb0a39 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -387,11 +387,14 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) throw new ArgumentNullException(nameof(endpoint)); } - var result = Connect(new List() { endpoint }, new EmptyTokenCredential()); + Credential ??= new EmptyTokenCredential(); + + Endpoints = new List() { endpoint }; + ConnectionStrings = null; IsCdnEnabled = true; - return result; + return this; } /// From 4a54b2ec232a6290afc609f27893b67b227da27a Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 17:11:09 -0700 Subject: [PATCH 45/65] nit --- .../Cdn/EmptyTokenCredential.cs | 5 ++++- .../Extensions/PageExtensions.cs | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs index 4ea6dba3..caf041da 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs @@ -1,4 +1,7 @@ -using Azure.Core; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Azure.Core; using System; using System.Threading; using System.Threading.Tasks; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs index 81f4fbf5..7cfcf8cb 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs @@ -1,7 +1,8 @@ -// write one generic extension method that takes a KeyValueChange object and returns a string representation of the key and value +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// using Azure; using Azure.Data.AppConfiguration; -using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models; namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions { From e5440489716a733f7b95984e69595aa5b8cec548 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 17:14:47 -0700 Subject: [PATCH 46/65] nit: add new lines --- .../Cdn/EmptyTokenCredential.cs | 2 +- .../Extensions/PageExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs index caf041da..0fb835e0 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs @@ -35,4 +35,4 @@ public override ValueTask GetTokenAsync(TokenRequestContext request return new ValueTask(new AccessToken(string.Empty, DateTimeOffset.MaxValue)); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs index 7cfcf8cb..bdb568fe 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs @@ -15,4 +15,4 @@ public static string GetCdnToken(this Page page) return response.Headers.ETag?.ToString(); } } -} \ No newline at end of file +} From ad5ac9b41a72d6fd10f64abbbfaf2872fcd50878 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 19:16:19 -0700 Subject: [PATCH 47/65] add refresh under cdn testing and fix bug --- .../AzureAppConfigurationSource.cs | 19 ++- .../RefreshTests.cs | 152 ++++++++++++++++++ .../Tests.AzureAppConfiguration/TestHelper.cs | 2 + 3 files changed, 165 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index aebdec6c..1bd66530 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -36,6 +36,17 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) { AzureAppConfigurationOptions options = _optionsProvider(); + if (options.IsCdnEnabled) + { + if (options.LoadBalancingEnabled) + { + throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); + } + + options.CdnTokenAccessor = new CdnTokenAccessor(); + options.ClientOptions.AddPolicy(new CdnPolicy(options.CdnTokenAccessor), HttpPipelinePosition.PerCall); + } + if (options.ClientManager != null) { return new AzureAppConfigurationProvider(options.ClientManager, options, _optional); @@ -63,14 +74,6 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) if (options.IsCdnEnabled) { - if (options.LoadBalancingEnabled) - { - throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); - } - - options.CdnTokenAccessor = new CdnTokenAccessor(); - options.ClientOptions.AddPolicy(new CdnPolicy(options.CdnTokenAccessor), HttpPipelinePosition.PerCall); - provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); } else diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs index c27a48ff..959acb40 100644 --- a/tests/Tests.AzureAppConfiguration/RefreshTests.cs +++ b/tests/Tests.AzureAppConfiguration/RefreshTests.cs @@ -1340,5 +1340,157 @@ Response GetIfChanged(ConfigurationSetting setting, bool o return mockClient; } + + [Fact] + public async Task RefreshTests_CdnWithCollectionMonitoring() + { + var keyValueCollection = new List(_kvCollection); + var mockResponse = new Mock(); + var mockClient = new Mock(MockBehavior.Strict); + + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(() => + { + var copy = new List(); + foreach (var setting in keyValueCollection) + { + copy.Add(TestHelpers.CloneSetting(setting)); + } + + return new MockAsyncPageable(copy); + }); + + IConfigurationRefresher refresher = null; + AzureAppConfigurationOptions capturedOptions = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ConnectCdn(TestHelpers.MockCdnEndpoint) + .Select("TestKey*") + .ConfigureRefresh(refreshOptions => + { + refreshOptions.RegisterAll() + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + + refresher = options.GetRefresher(); + capturedOptions = options; + }) + .Build(); + + Assert.Equal("TestValue1", config["TestKey1"]); + Assert.Equal("TestValue2", config["TestKey2"]); + Assert.Equal("TestValue3", config["TestKey3"]); + + // Verify CDN is enabled + Assert.True(capturedOptions.IsCdnEnabled); + + // Verify that current CDN token is null at startup + Assert.Null(capturedOptions.CdnTokenAccessor.Current); + + // Change the sentinel key to trigger a refresh + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to new page change ETag + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("newValue", config["TestKey1"]); + } + + [Fact] + public async Task RefreshTests_CdnWithSentinelKeys() + { + var keyValueCollection = new List(_kvCollection); + var mockResponse = new Mock(); + var mockClient = new Mock(MockBehavior.Strict); + + Response GetSettingFromService(string k, string l, CancellationToken ct) + { + return Response.FromValue(keyValueCollection.FirstOrDefault(s => s.Key == k && s.Label == l), mockResponse.Object); + } + + Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken) + { + var newSetting = keyValueCollection.FirstOrDefault(s => s.Key == setting.Key && s.Label == setting.Label); + var unchanged = (newSetting.Key == setting.Key && newSetting.Label == setting.Label && newSetting.Value == setting.Value); + var response = new MockResponse(unchanged ? 304 : 200); + return Response.FromValue(newSetting, response); + } + + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(() => + { + var copy = new List(); + foreach (var setting in keyValueCollection) + { + copy.Add(TestHelpers.CloneSetting(setting)); + } + + return new MockAsyncPageable(copy); + }); + + mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Func>)GetSettingFromService); + + mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Func>)GetIfChanged); + + IConfigurationRefresher refresher = null; + AzureAppConfigurationOptions capturedOptions = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ConnectCdn(TestHelpers.MockCdnEndpoint) + .Select("TestKey*") + .ConfigureRefresh(refreshOptions => + { + refreshOptions.Register("TestKey1", "label", refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + + refresher = options.GetRefresher(); + capturedOptions = options; + }) + .Build(); + + Assert.Equal("TestValue1", config["TestKey1"]); + Assert.Equal("TestValue2", config["TestKey2"]); + Assert.Equal("TestValue3", config["TestKey3"]); + + // Verify CDN is enabled + Assert.True(capturedOptions.IsCdnEnabled); + + // Verify that current CDN token is null at startup + Assert.Null(capturedOptions.CdnTokenAccessor.Current); + + // Change the sentinel key to trigger a refresh + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token is set to the new sentinel key etag + Assert.Equal(keyValueCollection[0].ETag.ToString(), capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("newValue", config["TestKey1"]); + } } } diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs index 9fd3f388..25ac2bc6 100644 --- a/tests/Tests.AzureAppConfiguration/TestHelper.cs +++ b/tests/Tests.AzureAppConfiguration/TestHelper.cs @@ -72,6 +72,8 @@ static public string CreateMockEndpointString(string endpoint = "https://azure.a return $"Endpoint={endpoint};Id=b1d9b31;Secret={returnValue}"; } + static public Uri MockCdnEndpoint => new Uri("https://cdn.azurefd.net"); + static public void SerializeSetting(ref Utf8JsonWriter json, ConfigurationSetting setting) { json.WriteStartObject(); From 33a9fb5484267c8788770d50fb52e0c81a09d509 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 19:47:40 -0700 Subject: [PATCH 48/65] make tests more robust --- .../Azure.Core.Testing/MockResponse.cs | 4 +- .../RefreshTests.cs | 89 ++++++++++++------- .../Tests.AzureAppConfiguration/TestHelper.cs | 6 +- 3 files changed, 65 insertions(+), 34 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs index c60c2a25..5322bb1d 100644 --- a/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs +++ b/tests/Tests.AzureAppConfiguration/Azure.Core.Testing/MockResponse.cs @@ -13,14 +13,14 @@ public class MockResponse : Response { private readonly Dictionary> _headers = new Dictionary>(StringComparer.OrdinalIgnoreCase); - public MockResponse(int status, string reasonPhrase = null) + public MockResponse(int status, string etag = null, string reasonPhrase = null) { Status = status; ReasonPhrase = reasonPhrase; if (status == 200) { - AddHeader(new HttpHeader(HttpHeader.Names.ETag, "\"" + Guid.NewGuid().ToString() + "\"")); + AddHeader(new HttpHeader(HttpHeader.Names.ETag, etag ?? "\"" + Guid.NewGuid().ToString() + "\"")); } } diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs index 959acb40..9444ba00 100644 --- a/tests/Tests.AzureAppConfiguration/RefreshTests.cs +++ b/tests/Tests.AzureAppConfiguration/RefreshTests.cs @@ -1347,18 +1347,10 @@ public async Task RefreshTests_CdnWithCollectionMonitoring() var keyValueCollection = new List(_kvCollection); var mockResponse = new Mock(); var mockClient = new Mock(MockBehavior.Strict); + var mockAsyncPageable = new MockAsyncPageable(keyValueCollection); mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) - .Returns(() => - { - var copy = new List(); - foreach (var setting in keyValueCollection) - { - copy.Add(TestHelpers.CloneSetting(setting)); - } - - return new MockAsyncPageable(copy); - }); + .Returns(mockAsyncPageable); IConfigurationRefresher refresher = null; AzureAppConfigurationOptions capturedOptions = null; @@ -1391,21 +1383,40 @@ public async Task RefreshTests_CdnWithCollectionMonitoring() // Verify that current CDN token is null at startup Assert.Null(capturedOptions.CdnTokenAccessor.Current); - // Change the sentinel key to trigger a refresh - keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); + // + // change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); - // Wait for the cache to expire - await Task.Delay(1500); + mockAsyncPageable.UpdateCollection(keyValueCollection); - // Trigger refresh - this should set a token in the CDN token accessor - await refresher.RefreshAsync(); + // Wait for the cache to expire + await Task.Delay(1500); - // Verify that the CDN token accessor has a token set to new page change ETag - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); - // Verify the configuration was updated - Assert.Equal("newValue", config["TestKey1"]); + // Verify that the CDN token accessor has a token set to new page change ETag + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("newValue", config["TestKey1"]); + } + + string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; + + // + // no change + { + // Wait for the cache to expire + await Task.Delay(1500); + + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to previous page change ETag + Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + } } [Fact] @@ -1477,20 +1488,36 @@ Response GetIfChanged(ConfigurationSetting setting, bool o // Verify that current CDN token is null at startup Assert.Null(capturedOptions.CdnTokenAccessor.Current); - // Change the sentinel key to trigger a refresh - keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); + // + // change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); - // Wait for the cache to expire - await Task.Delay(1500); + // Wait for the cache to expire + await Task.Delay(1500); - // Trigger refresh - this should set a token in the CDN token accessor - await refresher.RefreshAsync(); + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); - // Verify that the CDN token is set to the new sentinel key etag - Assert.Equal(keyValueCollection[0].ETag.ToString(), capturedOptions.CdnTokenAccessor.Current); + // Verify that the CDN token is set to the new sentinel key etag + Assert.Equal(keyValueCollection[0].ETag.ToString(), capturedOptions.CdnTokenAccessor.Current); - // Verify the configuration was updated - Assert.Equal("newValue", config["TestKey1"]); + // Verify the configuration was updated + Assert.Equal("newValue", config["TestKey1"]); + } + + string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; + + // + // no change + { + await Task.Delay(1500); + + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to previous CDN token + Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + } } } } diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs index 25ac2bc6..bbb06e6e 100644 --- a/tests/Tests.AzureAppConfiguration/TestHelper.cs +++ b/tests/Tests.AzureAppConfiguration/TestHelper.cs @@ -166,6 +166,7 @@ class MockAsyncPageable : AsyncPageable { private readonly List _collection = new List(); private int _status; + private string _etag; private readonly TimeSpan? _delay; public MockAsyncPageable(List collection, TimeSpan? delay = null) @@ -180,6 +181,7 @@ public MockAsyncPageable(List collection, TimeSpan? delay } _status = 200; + _etag = "\"" + Guid.NewGuid().ToString() + "\""; _delay = delay; } @@ -208,6 +210,8 @@ public void UpdateCollection(List newCollection) _collection.Add(newSetting); } + + _etag = "\"" + Guid.NewGuid().ToString() + "\""; } } @@ -218,7 +222,7 @@ public override async IAsyncEnumerable> AsPages(strin await Task.Delay(_delay.Value); } - yield return Page.FromValues(_collection, null, new MockResponse(_status)); + yield return Page.FromValues(_collection, null, new MockResponse(_status, _etag)); } } From 1abb1917f9e0f86e492039f199860daf0216448c Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Fri, 6 Jun 2025 19:50:21 -0700 Subject: [PATCH 49/65] nit --- tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs index 9444ba00..cc6d378e 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs @@ -1414,7 +1414,7 @@ public async Task RefreshTests_CdnWithCollectionMonitoring() await refresher.RefreshAsync(); - // Verify that the CDN token accessor has a token set to previous page change ETag + // Verify that the CDN token accessor has a token set to previous CDN token Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); } } From 911f815ac6ba00a6b274e94f9c9eab915cd72892 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 9 Jun 2025 15:02:05 -0700 Subject: [PATCH 50/65] nits --- .../AzureAppConfigurationOptions.cs | 2 +- .../AzureAppConfigurationProvider.cs | 19 +++++++++---------- .../Cdn/CdnPolicy.cs | 4 +++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 84cbecd8..2884dd8b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -163,7 +163,7 @@ internal IEnumerable Adapters /// /// Gets a value indicating whether CDN is enabled. /// - internal bool IsCdnEnabled { get; private set; } = false; + internal bool IsCdnEnabled { get; private set; } /// /// Initializes a new instance of the class. diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 7b4f145e..033fa893 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -16,7 +16,6 @@ using System.Net.Http; using System.Net.Sockets; using System.Security; -using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -43,8 +42,8 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura private DateTimeOffset _nextCollectionRefreshTime; #region Cdn - private string _configVersion = null; - private string _ffCollectionVersion = null; + private string _configCdnToken = null; + private string _ffCdnToken = null; #endregion private readonly TimeSpan MinRefreshInterval; @@ -317,7 +316,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => if (_options.IsCdnEnabled) { - _options.CdnTokenAccessor.Current = _configVersion; + _options.CdnTokenAccessor.Current = _configCdnToken; } if (_options.RegisterAllEnabled) @@ -353,8 +352,8 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa // // Reset versions so that next watch request will not use stale versions. - _configVersion = token; - _ffCollectionVersion = token; + _configCdnToken = token; + _ffCdnToken = token; } break; @@ -396,8 +395,8 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa // // Reset versions so that next watch request will not use stale versions. - _configVersion = token; - _ffCollectionVersion = token; + _configCdnToken = token; + _ffCdnToken = token; } break; @@ -422,7 +421,7 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa // Get feature flag changes if (_options.IsCdnEnabled) { - _options.CdnTokenAccessor.Current = _ffCollectionVersion; + _options.CdnTokenAccessor.Current = _ffCdnToken; } var ffSelectors = refreshableFfWatchers.Select(watcher => new KeyValueSelector @@ -460,7 +459,7 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa // // Reset ff collection version so that next ff watch request will not use stale version. - _ffCollectionVersion = token; + _ffCdnToken = token; } break; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs index 2b21650b..a0c24f4a 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs @@ -15,6 +15,8 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn /// internal class CdnPolicy : HttpPipelinePolicy { + private const string CdnTokenQueryParameter = "_"; + private readonly ICdnTokenAccessor _cdnTokenAccessor; /// @@ -70,7 +72,7 @@ private static Uri AddTokenToUri(Uri uri, string token) var uriBuilder = new UriBuilder(uri); var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query["_"] = token; + query[CdnTokenQueryParameter] = token; uriBuilder.Query = query.ToString(); From 568a7e87a2fdbfe6ce39763a3718055ba4d0f2aa Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 9 Jun 2025 15:17:43 -0700 Subject: [PATCH 51/65] ensure test is inductive --- .../Unit/RefreshTests.cs | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs index cc6d378e..913c95bd 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs @@ -1396,7 +1396,7 @@ public async Task RefreshTests_CdnWithCollectionMonitoring() // Trigger refresh - this should set a token in the CDN token accessor await refresher.RefreshAsync(); - // Verify that the CDN token accessor has a token set to new page change ETag + // Verify that the CDN token accessor has a token set to new value Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); @@ -1417,6 +1417,28 @@ public async Task RefreshTests_CdnWithCollectionMonitoring() // Verify that the CDN token accessor has a token set to previous CDN token Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); } + + // + // another change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "anotherNewValue"); + + mockAsyncPageable.UpdateCollection(keyValueCollection); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to new value + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("anotherNewValue", config["TestKey1"]); + } } [Fact] @@ -1499,8 +1521,9 @@ Response GetIfChanged(ConfigurationSetting setting, bool o // Trigger refresh - this should set a token in the CDN token accessor await refresher.RefreshAsync(); - // Verify that the CDN token is set to the new sentinel key etag - Assert.Equal(keyValueCollection[0].ETag.ToString(), capturedOptions.CdnTokenAccessor.Current); + // Verify that the CDN token is set to the new value + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); // Verify the configuration was updated Assert.Equal("newValue", config["TestKey1"]); @@ -1518,6 +1541,26 @@ Response GetIfChanged(ConfigurationSetting setting, bool o // Verify that the CDN token accessor has a token set to previous CDN token Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); } + + // + // another change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "anotherNewValue"); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to new value + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("anotherNewValue", config["TestKey1"]); + } } } } From 48802690bacffc2758612c6b436bd4d73faea20a Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 9 Jun 2025 21:14:26 -0700 Subject: [PATCH 52/65] handle ConnectCdn with SetClientFactory scenario --- .../AzureAppConfigurationOptions.cs | 18 ++++- .../AzureAppConfigurationSource.cs | 9 ++- .../Unit/ConnectCdnTests.cs | 77 +++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 2884dd8b..c98abe28 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -128,7 +128,7 @@ internal IEnumerable Adapters /// /// Options used to configure the client used to communicate with Azure App Configuration. /// - internal ConfigurationClientOptions ClientOptions { get; } = GetDefaultClientOptions(); + internal ConfigurationClientOptions ClientOptions { get; private set; } = GetDefaultClientOptions(); /// /// Flag to indicate whether Key Vault options have been configured. @@ -155,6 +155,11 @@ internal IEnumerable Adapters /// internal IAzureClientFactory ClientFactory { get; private set; } + /// + /// Gets a value indicating whether a ConfigurationClientOptions options was provided when setting custom client factory. + /// + internal bool IsClientOptionsProvided { get; private set; } + /// /// An accessor for current token to be used for CDN cache breakage/consistency. /// @@ -188,10 +193,18 @@ public AzureAppConfigurationOptions() /// will not be used to authenticate a . /// /// The client factory. + /// Optional client options that user should provide when CDN is enabled. /// The current instance. - public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory factory) + public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory factory, ConfigurationClientOptions options = null) { ClientFactory = factory ?? throw new ArgumentNullException(nameof(factory)); + + if (options != null) + { + IsClientOptionsProvided = true; + ClientOptions = options; + } + return this; } @@ -411,6 +424,7 @@ public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) ConnectionStrings = null; IsCdnEnabled = true; + CdnTokenAccessor = new CdnTokenAccessor(); return this; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 1bd66530..57b17ddc 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -36,6 +36,8 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) { AzureAppConfigurationOptions options = _optionsProvider(); + IAzureClientFactory clientFactory = options.ClientFactory; + if (options.IsCdnEnabled) { if (options.LoadBalancingEnabled) @@ -43,7 +45,11 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); } - options.CdnTokenAccessor = new CdnTokenAccessor(); + if (clientFactory != null && !options.IsClientOptionsProvided) + { + throw new InvalidOperationException($"Please provide the optional param {nameof(ConfigurationClientOptions)} when calling {nameof(AzureAppConfigurationOptions.SetClientFactory)} with CDN."); + } + options.ClientOptions.AddPolicy(new CdnPolicy(options.CdnTokenAccessor), HttpPipelinePosition.PerCall); } @@ -53,7 +59,6 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } IEnumerable endpoints; - IAzureClientFactory clientFactory = options.ClientFactory; if (options.ConnectionStrings != null) { diff --git a/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs new file mode 100644 index 00000000..eac5633e --- /dev/null +++ b/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs @@ -0,0 +1,77 @@ +using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading; +using Xunit; + +namespace Tests.AzureAppConfiguration +{ + public class ConnectCdnTests + { + [Fact] + public void ConnectCdnTests_CdnWithClientFactoryRequiresClientOptions() + { + var mockClientFactory = new Mock>(); + + var configBuilder = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.SetClientFactory(mockClientFactory.Object) // No client options provided + .ConnectCdn(TestHelpers.MockCdnEndpoint); + }); + + Exception exception = Assert.Throws(() => configBuilder.Build()); + Assert.IsType(exception.InnerException); + } + + [Fact] + public void ConnectCdnTests_CdnWithClientFactoryAndClientOptionsSucceeds() + { + var mockClientFactory = new Mock>(); + var mockClient = new Mock(MockBehavior.Strict); + var clientOptions = new ConfigurationClientOptions(); + + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(new List())); + + mockClientFactory.Setup(f => f.CreateClient(It.IsAny())) + .Returns(mockClient.Object); + + AzureAppConfigurationOptions capturedOptions = null; + + var configBuilder = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.SetClientFactory(mockClientFactory.Object, clientOptions) // Client options provided + .ConnectCdn(TestHelpers.MockCdnEndpoint); + capturedOptions = options; + }); + + Assert.NotNull(configBuilder.Build()); + + Assert.NotNull(capturedOptions); + Assert.True(capturedOptions.IsCdnEnabled); + Assert.Equal(clientOptions, capturedOptions.ClientOptions); + + mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void ConnectCdnTests_DoesNotSupportLoadBalancing() + { + var configBuilder = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ConnectCdn(TestHelpers.MockCdnEndpoint) + .LoadBalancingEnabled = true; + }); + + Exception exception = Assert.Throws(() => configBuilder.Build()); + Assert.IsType(exception.InnerException); + } + } +} \ No newline at end of file From 23acffff4f48c87f832c329c3e031680ae134a6e Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 9 Jun 2025 21:18:20 -0700 Subject: [PATCH 53/65] nit: add ms license --- tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs index eac5633e..7210f7b5 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; @@ -74,4 +77,4 @@ public void ConnectCdnTests_DoesNotSupportLoadBalancing() Assert.IsType(exception.InnerException); } } -} \ No newline at end of file +} From bd487dcb4e6a3088472599f98d785f5201272598 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 9 Jun 2025 23:17:58 -0700 Subject: [PATCH 54/65] move all cdn related tests to cdn tests --- .../Unit/CdnTests.cs | 344 ++++++++++++++++++ .../Unit/ConnectCdnTests.cs | 80 ---- .../Unit/RefreshTests.cs | 222 ----------- 3 files changed, 344 insertions(+), 302 deletions(-) create mode 100644 tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs delete mode 100644 tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs diff --git a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs new file mode 100644 index 00000000..c861158a --- /dev/null +++ b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs @@ -0,0 +1,344 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +using Azure; +using Azure.Core.Testing; +using Azure.Data.AppConfiguration; +using Microsoft.Extensions.Azure; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.AzureAppConfiguration; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Tests.AzureAppConfiguration +{ + public class CdnTests + { + List _kvCollection = new List + { + ConfigurationModelFactory.ConfigurationSetting( + key: "TestKey1", + label: "label", + value: "TestValue1", + eTag: new ETag("0a76e3d7-7ec1-4e37-883c-9ea6d0d89e63"), + contentType: "text"), + + ConfigurationModelFactory.ConfigurationSetting( + key: "TestKey2", + label: "label", + value: "TestValue2", + eTag: new ETag("31c38369-831f-4bf1-b9ad-79db56c8b989"), + contentType: "text"), + + ConfigurationModelFactory.ConfigurationSetting( + key: "TestKey3", + label: "label", + value: "TestValue3", + eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"), + contentType: "text"), + + ConfigurationModelFactory.ConfigurationSetting( + key: "TestKeyWithMultipleLabels", + label: "label1", + value: "TestValueForLabel1", + eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"), + contentType: "text"), + + ConfigurationModelFactory.ConfigurationSetting( + key: "TestKeyWithMultipleLabels", + label: "label2", + value: "TestValueForLabel2", + eTag: new ETag("bb203f2b-c113-44fc-995d-b933c2143339"), + contentType: "text") + }; + + [Fact] + public void CdnTests_CdnWithClientFactoryRequiresClientOptions() + { + var mockClientFactory = new Mock>(); + + var configBuilder = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.SetClientFactory(mockClientFactory.Object) // No client options provided + .ConnectCdn(TestHelpers.MockCdnEndpoint); + }); + + Exception exception = Assert.Throws(() => configBuilder.Build()); + Assert.IsType(exception.InnerException); + } + + [Fact] + public void CdnTests_CdnWithClientFactoryAndClientOptionsSucceeds() + { + var mockClientFactory = new Mock>(); + var mockClient = new Mock(MockBehavior.Strict); + var clientOptions = new ConfigurationClientOptions(); + + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(new MockAsyncPageable(new List())); + + mockClientFactory.Setup(f => f.CreateClient(It.IsAny())) + .Returns(mockClient.Object); + + AzureAppConfigurationOptions capturedOptions = null; + + var configBuilder = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.SetClientFactory(mockClientFactory.Object, clientOptions) // Client options provided + .ConnectCdn(TestHelpers.MockCdnEndpoint); + capturedOptions = options; + }); + + Assert.NotNull(configBuilder.Build()); + + Assert.NotNull(capturedOptions); + Assert.True(capturedOptions.IsCdnEnabled); + Assert.Equal(clientOptions, capturedOptions.ClientOptions); + + mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public void CdnTests_DoesNotSupportLoadBalancing() + { + var configBuilder = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ConnectCdn(TestHelpers.MockCdnEndpoint) + .LoadBalancingEnabled = true; + }); + + Exception exception = Assert.Throws(() => configBuilder.Build()); + Assert.IsType(exception.InnerException); + } + + [Fact] + public async Task CdnTests_RefreshWithRegisterAll() + { + var keyValueCollection = new List(_kvCollection); + var mockResponse = new Mock(); + var mockClient = new Mock(MockBehavior.Strict); + var mockAsyncPageable = new MockAsyncPageable(keyValueCollection); + + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(mockAsyncPageable); + + IConfigurationRefresher refresher = null; + AzureAppConfigurationOptions capturedOptions = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ConnectCdn(TestHelpers.MockCdnEndpoint) + .Select("TestKey*") + .ConfigureRefresh(refreshOptions => + { + refreshOptions.RegisterAll() + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + + refresher = options.GetRefresher(); + capturedOptions = options; + }) + .Build(); + + Assert.Equal("TestValue1", config["TestKey1"]); + Assert.Equal("TestValue2", config["TestKey2"]); + Assert.Equal("TestValue3", config["TestKey3"]); + + // Verify CDN is enabled + Assert.True(capturedOptions.IsCdnEnabled); + + // Verify that current CDN token is null at startup + Assert.Null(capturedOptions.CdnTokenAccessor.Current); + + // + // change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); + + mockAsyncPageable.UpdateCollection(keyValueCollection); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to new value + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("newValue", config["TestKey1"]); + } + + string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; + + // + // no change + { + // Wait for the cache to expire + await Task.Delay(1500); + + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to previous CDN token + Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + } + + // + // another change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "anotherNewValue"); + + mockAsyncPageable.UpdateCollection(keyValueCollection); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to new value + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("anotherNewValue", config["TestKey1"]); + } + } + + [Fact] + public async Task CdnTests_RefreshWithRegister() + { + var keyValueCollection = new List(_kvCollection); + var mockResponse = new Mock(); + var mockClient = new Mock(MockBehavior.Strict); + + Response GetSettingFromService(string k, string l, CancellationToken ct) + { + return Response.FromValue(keyValueCollection.FirstOrDefault(s => s.Key == k && s.Label == l), mockResponse.Object); + } + + Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken) + { + var newSetting = keyValueCollection.FirstOrDefault(s => s.Key == setting.Key && s.Label == setting.Label); + var unchanged = (newSetting.Key == setting.Key && newSetting.Label == setting.Label && newSetting.Value == setting.Value); + var response = new MockResponse(unchanged ? 304 : 200); + return Response.FromValue(newSetting, response); + } + + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(() => + { + var copy = new List(); + foreach (var setting in keyValueCollection) + { + copy.Add(TestHelpers.CloneSetting(setting)); + } + + return new MockAsyncPageable(copy); + }); + + mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Func>)GetSettingFromService); + + mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Func>)GetIfChanged); + + IConfigurationRefresher refresher = null; + AzureAppConfigurationOptions capturedOptions = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ConnectCdn(TestHelpers.MockCdnEndpoint) + .Select("TestKey*") + .ConfigureRefresh(refreshOptions => + { + refreshOptions.Register("TestKey1", "label", refreshAll: true) + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + + refresher = options.GetRefresher(); + capturedOptions = options; + }) + .Build(); + + Assert.Equal("TestValue1", config["TestKey1"]); + Assert.Equal("TestValue2", config["TestKey2"]); + Assert.Equal("TestValue3", config["TestKey3"]); + + // Verify CDN is enabled + Assert.True(capturedOptions.IsCdnEnabled); + + // Verify that current CDN token is null at startup + Assert.Null(capturedOptions.CdnTokenAccessor.Current); + + // + // change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token is set to the new value + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("newValue", config["TestKey1"]); + } + + string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; + + // + // no change + { + await Task.Delay(1500); + + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to previous CDN token + Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + } + + // + // another change + { + keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "anotherNewValue"); + + // Wait for the cache to expire + await Task.Delay(1500); + + // Trigger refresh - this should set a token in the CDN token accessor + await refresher.RefreshAsync(); + + // Verify that the CDN token accessor has a token set to new value + Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + + // Verify the configuration was updated + Assert.Equal("anotherNewValue", config["TestKey1"]); + } + } + } +} diff --git a/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs deleted file mode 100644 index 7210f7b5..00000000 --- a/tests/Tests.AzureAppConfiguration/Unit/ConnectCdnTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. -// -using Azure.Data.AppConfiguration; -using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.AzureAppConfiguration; -using Moq; -using System; -using System.Collections.Generic; -using System.Threading; -using Xunit; - -namespace Tests.AzureAppConfiguration -{ - public class ConnectCdnTests - { - [Fact] - public void ConnectCdnTests_CdnWithClientFactoryRequiresClientOptions() - { - var mockClientFactory = new Mock>(); - - var configBuilder = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.SetClientFactory(mockClientFactory.Object) // No client options provided - .ConnectCdn(TestHelpers.MockCdnEndpoint); - }); - - Exception exception = Assert.Throws(() => configBuilder.Build()); - Assert.IsType(exception.InnerException); - } - - [Fact] - public void ConnectCdnTests_CdnWithClientFactoryAndClientOptionsSucceeds() - { - var mockClientFactory = new Mock>(); - var mockClient = new Mock(MockBehavior.Strict); - var clientOptions = new ConfigurationClientOptions(); - - mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) - .Returns(new MockAsyncPageable(new List())); - - mockClientFactory.Setup(f => f.CreateClient(It.IsAny())) - .Returns(mockClient.Object); - - AzureAppConfigurationOptions capturedOptions = null; - - var configBuilder = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.SetClientFactory(mockClientFactory.Object, clientOptions) // Client options provided - .ConnectCdn(TestHelpers.MockCdnEndpoint); - capturedOptions = options; - }); - - Assert.NotNull(configBuilder.Build()); - - Assert.NotNull(capturedOptions); - Assert.True(capturedOptions.IsCdnEnabled); - Assert.Equal(clientOptions, capturedOptions.ClientOptions); - - mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public void ConnectCdnTests_DoesNotSupportLoadBalancing() - { - var configBuilder = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.ConnectCdn(TestHelpers.MockCdnEndpoint) - .LoadBalancingEnabled = true; - }); - - Exception exception = Assert.Throws(() => configBuilder.Build()); - Assert.IsType(exception.InnerException); - } - } -} diff --git a/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs index 913c95bd..c27a48ff 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/RefreshTests.cs @@ -1340,227 +1340,5 @@ Response GetIfChanged(ConfigurationSetting setting, bool o return mockClient; } - - [Fact] - public async Task RefreshTests_CdnWithCollectionMonitoring() - { - var keyValueCollection = new List(_kvCollection); - var mockResponse = new Mock(); - var mockClient = new Mock(MockBehavior.Strict); - var mockAsyncPageable = new MockAsyncPageable(keyValueCollection); - - mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) - .Returns(mockAsyncPageable); - - IConfigurationRefresher refresher = null; - AzureAppConfigurationOptions capturedOptions = null; - - var config = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.ConnectCdn(TestHelpers.MockCdnEndpoint) - .Select("TestKey*") - .ConfigureRefresh(refreshOptions => - { - refreshOptions.RegisterAll() - .SetRefreshInterval(TimeSpan.FromSeconds(1)); - }); - - options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); - - refresher = options.GetRefresher(); - capturedOptions = options; - }) - .Build(); - - Assert.Equal("TestValue1", config["TestKey1"]); - Assert.Equal("TestValue2", config["TestKey2"]); - Assert.Equal("TestValue3", config["TestKey3"]); - - // Verify CDN is enabled - Assert.True(capturedOptions.IsCdnEnabled); - - // Verify that current CDN token is null at startup - Assert.Null(capturedOptions.CdnTokenAccessor.Current); - - // - // change - { - keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); - - mockAsyncPageable.UpdateCollection(keyValueCollection); - - // Wait for the cache to expire - await Task.Delay(1500); - - // Trigger refresh - this should set a token in the CDN token accessor - await refresher.RefreshAsync(); - - // Verify that the CDN token accessor has a token set to new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); - - // Verify the configuration was updated - Assert.Equal("newValue", config["TestKey1"]); - } - - string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; - - // - // no change - { - // Wait for the cache to expire - await Task.Delay(1500); - - await refresher.RefreshAsync(); - - // Verify that the CDN token accessor has a token set to previous CDN token - Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); - } - - // - // another change - { - keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "anotherNewValue"); - - mockAsyncPageable.UpdateCollection(keyValueCollection); - - // Wait for the cache to expire - await Task.Delay(1500); - - // Trigger refresh - this should set a token in the CDN token accessor - await refresher.RefreshAsync(); - - // Verify that the CDN token accessor has a token set to new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); - - // Verify the configuration was updated - Assert.Equal("anotherNewValue", config["TestKey1"]); - } - } - - [Fact] - public async Task RefreshTests_CdnWithSentinelKeys() - { - var keyValueCollection = new List(_kvCollection); - var mockResponse = new Mock(); - var mockClient = new Mock(MockBehavior.Strict); - - Response GetSettingFromService(string k, string l, CancellationToken ct) - { - return Response.FromValue(keyValueCollection.FirstOrDefault(s => s.Key == k && s.Label == l), mockResponse.Object); - } - - Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken) - { - var newSetting = keyValueCollection.FirstOrDefault(s => s.Key == setting.Key && s.Label == setting.Label); - var unchanged = (newSetting.Key == setting.Key && newSetting.Label == setting.Label && newSetting.Value == setting.Value); - var response = new MockResponse(unchanged ? 304 : 200); - return Response.FromValue(newSetting, response); - } - - mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) - .Returns(() => - { - var copy = new List(); - foreach (var setting in keyValueCollection) - { - copy.Add(TestHelpers.CloneSetting(setting)); - } - - return new MockAsyncPageable(copy); - }); - - mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync((Func>)GetSettingFromService); - - mockClient.Setup(c => c.GetConfigurationSettingAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync((Func>)GetIfChanged); - - IConfigurationRefresher refresher = null; - AzureAppConfigurationOptions capturedOptions = null; - - var config = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.ConnectCdn(TestHelpers.MockCdnEndpoint) - .Select("TestKey*") - .ConfigureRefresh(refreshOptions => - { - refreshOptions.Register("TestKey1", "label", refreshAll: true) - .SetRefreshInterval(TimeSpan.FromSeconds(1)); - }); - - options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); - - refresher = options.GetRefresher(); - capturedOptions = options; - }) - .Build(); - - Assert.Equal("TestValue1", config["TestKey1"]); - Assert.Equal("TestValue2", config["TestKey2"]); - Assert.Equal("TestValue3", config["TestKey3"]); - - // Verify CDN is enabled - Assert.True(capturedOptions.IsCdnEnabled); - - // Verify that current CDN token is null at startup - Assert.Null(capturedOptions.CdnTokenAccessor.Current); - - // - // change - { - keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "newValue"); - - // Wait for the cache to expire - await Task.Delay(1500); - - // Trigger refresh - this should set a token in the CDN token accessor - await refresher.RefreshAsync(); - - // Verify that the CDN token is set to the new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); - - // Verify the configuration was updated - Assert.Equal("newValue", config["TestKey1"]); - } - - string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; - - // - // no change - { - await Task.Delay(1500); - - await refresher.RefreshAsync(); - - // Verify that the CDN token accessor has a token set to previous CDN token - Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); - } - - // - // another change - { - keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "anotherNewValue"); - - // Wait for the cache to expire - await Task.Delay(1500); - - // Trigger refresh - this should set a token in the CDN token accessor - await refresher.RefreshAsync(); - - // Verify that the CDN token accessor has a token set to new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); - - // Verify the configuration was updated - Assert.Equal("anotherNewValue", config["TestKey1"]); - } - } } } From 2c2a4134c53cad507b5d4feb8979a841397291ce Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 10 Jun 2025 04:09:20 -0700 Subject: [PATCH 55/65] add parallel test --- .../Unit/CdnTests.cs | 130 +++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs index c861158a..f69a9941 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // using Azure; @@ -340,5 +340,133 @@ Response GetIfChanged(ConfigurationSetting setting, bool o Assert.Equal("anotherNewValue", config["TestKey1"]); } } + + [Fact] + public async Task CdnTests_ParallelAppsHaveSameCdnTokenSequence() + { + var mockAsyncPageable = new MockAsyncPageable(_kvCollection.ToList()); + + // async coordination: Both apps are ready => wait for two releases + var startupSync = new SemaphoreSlim(0, 2); + var noChangeSync = new SemaphoreSlim(0, 2); + + // broadcast gates: coordinator releases twice, each app awaits once + var firstChangeGate = new SemaphoreSlim(0, 2); + var noChangeGate = new SemaphoreSlim(0, 2); + var secondChangeGate = new SemaphoreSlim(0, 2); + + async Task CreateAppTask(List cdnTokenList) + { + var mockClient = new Mock(MockBehavior.Strict); + + // Both clients use the same shared pageable for consistency + mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) + .Returns(() => mockAsyncPageable); + + IConfigurationRefresher refresher = null; + AzureAppConfigurationOptions capturedOptions = null; + + var config = new ConfigurationBuilder() + .AddAzureAppConfiguration(options => + { + options.ConnectCdn(TestHelpers.MockCdnEndpoint) + .Select("TestKey*") + .ConfigureRefresh(refreshOptions => + { + refreshOptions.RegisterAll() + .SetRefreshInterval(TimeSpan.FromSeconds(1)); + }); + + options.ClientManager = TestHelpers.CreateMockedConfigurationClientManager(mockClient.Object); + + refresher = options.GetRefresher(); + capturedOptions = options; + }) + .Build(); + + // Initial state - CDN token should be null + cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + + // Signal that this app is initialized + startupSync.Release(); + + // Wait for first change to be applied + await firstChangeGate.WaitAsync(); + await Task.Delay(1500); + await refresher.RefreshAsync(); + + cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + + // No change (should keep same token) + await noChangeGate.WaitAsync(); + await Task.Delay(1500); + await refresher.RefreshAsync(); + + cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + + // Signal that this app is done with no-change refresh + noChangeSync.Release(); + + // Wait for second change to be applied + await secondChangeGate.WaitAsync(); + await Task.Delay(1500); + await refresher.RefreshAsync(); + + cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + } + + var changeTask = Task.Run(async () => + { + // First change + await Task.WhenAll(startupSync.WaitAsync(), startupSync.WaitAsync()); // Wait for both apps to complete startup + var updatedCollection = _kvCollection.ToList(); + updatedCollection[0] = TestHelpers.ChangeValue(updatedCollection[0], "newValue"); + mockAsyncPageable.UpdateCollection(updatedCollection); + + firstChangeGate.Release(2); + + // No change + noChangeGate.Release(2); + + // Second change + await noChangeSync.WaitAsync(); // Wait for both apps to complete no-change refresh + updatedCollection = _kvCollection.ToList(); + updatedCollection[0] = TestHelpers.ChangeValue(updatedCollection[0], "anotherNewValue"); + mockAsyncPageable.UpdateCollection(updatedCollection); + + secondChangeGate.Release(2); + }); + + // Run both apps in parallel along with the change coordinator + var app1CdnTokens = new List(); + var app2CdnTokens = new List(); + var task1 = CreateAppTask(app1CdnTokens); + var task2 = CreateAppTask(app2CdnTokens); + + await Task.WhenAll(task1, task2, changeTask); + + // Verify both apps captured the same number of tokens + Assert.Equal(4, app1CdnTokens.Count); + Assert.Equal(4, app2CdnTokens.Count); + + // Verify the CDN token sequences are identical between the two apps + for (int i = 0; i < app1CdnTokens.Count; i++) + { + Assert.True(app1CdnTokens[i] == app2CdnTokens[i]); + } + + // Verify the expected token pattern: + // Index 0: null (initial state) + // Index 1: non-null (after first change) + // Index 2: same as index 1 (no change) + // Index 3: non-null and different from index 1 (after second change) + Assert.Null(app1CdnTokens[0]); + Assert.NotNull(app1CdnTokens[1]); + Assert.NotEmpty(app1CdnTokens[1]); + Assert.Equal(app1CdnTokens[1], app1CdnTokens[2]); + Assert.NotNull(app1CdnTokens[3]); + Assert.NotEmpty(app1CdnTokens[3]); + Assert.NotEqual(app1CdnTokens[1], app1CdnTokens[3]); + } } } From d8f280aed202e9ae8ab2a0e27c0b897a2895eab3 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 10 Jun 2025 04:15:13 -0700 Subject: [PATCH 56/65] fix bug --- tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs index f69a9941..738a9efc 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs @@ -429,7 +429,7 @@ async Task CreateAppTask(List cdnTokenList) noChangeGate.Release(2); // Second change - await noChangeSync.WaitAsync(); // Wait for both apps to complete no-change refresh + await Task.WhenAll(noChangeSync.WaitAsync(), noChangeSync.WaitAsync()); ; // Wait for both apps to complete no-change refresh updatedCollection = _kvCollection.ToList(); updatedCollection[0] = TestHelpers.ChangeValue(updatedCollection[0], "anotherNewValue"); mockAsyncPageable.UpdateCollection(updatedCollection); From ae0e448369627649e1f28eb9e87952d00dba15e9 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Tue, 10 Jun 2025 04:30:54 -0700 Subject: [PATCH 57/65] tests: add delete sentinel key to test --- .../Unit/CdnTests.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs index 738a9efc..48d6c5d5 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs @@ -231,12 +231,16 @@ Response GetSettingFromService(string k, string l, Cancell return Response.FromValue(keyValueCollection.FirstOrDefault(s => s.Key == k && s.Label == l), mockResponse.Object); } - Response GetIfChanged(ConfigurationSetting setting, bool onlyIfChanged, CancellationToken cancellationToken) + Response GetIfChanged(ConfigurationSetting setting, bool _, CancellationToken cancellationToken) { - var newSetting = keyValueCollection.FirstOrDefault(s => s.Key == setting.Key && s.Label == setting.Label); - var unchanged = (newSetting.Key == setting.Key && newSetting.Label == setting.Label && newSetting.Value == setting.Value); - var response = new MockResponse(unchanged ? 304 : 200); - return Response.FromValue(newSetting, response); + var currentSetting = keyValueCollection.FirstOrDefault(s => s.Key == setting.Key && s.Label == setting.Label); + + if (currentSetting == null) + { + throw new RequestFailedException(new MockResponse(404)); + } + + return Response.FromValue(currentSetting, new MockResponse(200)); } mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) @@ -321,9 +325,9 @@ Response GetIfChanged(ConfigurationSetting setting, bool o } // - // another change + // another change: sentinel deleted { - keyValueCollection[0] = TestHelpers.ChangeValue(keyValueCollection[0], "anotherNewValue"); + keyValueCollection.Remove(keyValueCollection[0]); // Wait for the cache to expire await Task.Delay(1500); @@ -337,7 +341,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool o Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); // Verify the configuration was updated - Assert.Equal("anotherNewValue", config["TestKey1"]); + Assert.Null(config["TestKey1"]); } } From feaec538e5e2b56087282b454320b4f47065ebe4 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 11 Jun 2025 17:41:08 -0700 Subject: [PATCH 58/65] done --- .../AzureAppConfigurationOptions.cs | 16 +------ .../AzureAppConfigurationSource.cs | 6 +-- .../Unit/CdnTests.cs | 46 +++---------------- 3 files changed, 12 insertions(+), 56 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index c98abe28..860891f1 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -155,11 +155,6 @@ internal IEnumerable Adapters /// internal IAzureClientFactory ClientFactory { get; private set; } - /// - /// Gets a value indicating whether a ConfigurationClientOptions options was provided when setting custom client factory. - /// - internal bool IsClientOptionsProvided { get; private set; } - /// /// An accessor for current token to be used for CDN cache breakage/consistency. /// @@ -193,18 +188,11 @@ public AzureAppConfigurationOptions() /// will not be used to authenticate a . /// /// The client factory. - /// Optional client options that user should provide when CDN is enabled. /// The current instance. - public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory factory, ConfigurationClientOptions options = null) + public AzureAppConfigurationOptions SetClientFactory(IAzureClientFactory factory) { ClientFactory = factory ?? throw new ArgumentNullException(nameof(factory)); - if (options != null) - { - IsClientOptionsProvided = true; - ClientOptions = options; - } - return this; } @@ -406,7 +394,7 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// Connect the provider to CDN endpoint. /// /// The endpoint of the CDN instance to connect to. - public AzureAppConfigurationOptions ConnectCdn(Uri endpoint) + public AzureAppConfigurationOptions ConnectAzureFrontDoor(Uri endpoint) { if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 57b17ddc..dc614a51 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -45,9 +45,9 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); } - if (clientFactory != null && !options.IsClientOptionsProvided) + if (clientFactory != null) { - throw new InvalidOperationException($"Please provide the optional param {nameof(ConfigurationClientOptions)} when calling {nameof(AzureAppConfigurationOptions.SetClientFactory)} with CDN."); + throw new InvalidOperationException($"Custom client factory is not supported when connecting to CDN."); } options.ClientOptions.AddPolicy(new CdnPolicy(options.CdnTokenAccessor), HttpPipelinePosition.PerCall); @@ -74,7 +74,7 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) } else { - throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} or {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.ConnectCdn)} to specify how to connect to Azure App Configuration."); + throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} or {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.ConnectAzureFrontDoor)} to specify how to connect to Azure App Configuration."); } if (options.IsCdnEnabled) diff --git a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs index 48d6c5d5..5dc58773 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs @@ -58,60 +58,28 @@ public class CdnTests }; [Fact] - public void CdnTests_CdnWithClientFactoryRequiresClientOptions() + public void CdnTests_DoesNotSupportCustomClientFactory() { var mockClientFactory = new Mock>(); var configBuilder = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.SetClientFactory(mockClientFactory.Object) // No client options provided - .ConnectCdn(TestHelpers.MockCdnEndpoint); + options.SetClientFactory(mockClientFactory.Object) + .ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint); }); Exception exception = Assert.Throws(() => configBuilder.Build()); Assert.IsType(exception.InnerException); } - [Fact] - public void CdnTests_CdnWithClientFactoryAndClientOptionsSucceeds() - { - var mockClientFactory = new Mock>(); - var mockClient = new Mock(MockBehavior.Strict); - var clientOptions = new ConfigurationClientOptions(); - - mockClient.Setup(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny())) - .Returns(new MockAsyncPageable(new List())); - - mockClientFactory.Setup(f => f.CreateClient(It.IsAny())) - .Returns(mockClient.Object); - - AzureAppConfigurationOptions capturedOptions = null; - - var configBuilder = new ConfigurationBuilder() - .AddAzureAppConfiguration(options => - { - options.SetClientFactory(mockClientFactory.Object, clientOptions) // Client options provided - .ConnectCdn(TestHelpers.MockCdnEndpoint); - capturedOptions = options; - }); - - Assert.NotNull(configBuilder.Build()); - - Assert.NotNull(capturedOptions); - Assert.True(capturedOptions.IsCdnEnabled); - Assert.Equal(clientOptions, capturedOptions.ClientOptions); - - mockClient.Verify(c => c.GetConfigurationSettingsAsync(It.IsAny(), It.IsAny()), Times.Once); - } - [Fact] public void CdnTests_DoesNotSupportLoadBalancing() { var configBuilder = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectCdn(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) .LoadBalancingEnabled = true; }); @@ -136,7 +104,7 @@ public async Task CdnTests_RefreshWithRegisterAll() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectCdn(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) .Select("TestKey*") .ConfigureRefresh(refreshOptions => { @@ -267,7 +235,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool _ var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectCdn(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) .Select("TestKey*") .ConfigureRefresh(refreshOptions => { @@ -373,7 +341,7 @@ async Task CreateAppTask(List cdnTokenList) var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectCdn(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) .Select("TestKey*") .ConfigureRefresh(refreshOptions => { From 3f71dd4a2ee82e66fbad3b365303518f4c8cf6c7 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 11 Jun 2025 17:46:36 -0700 Subject: [PATCH 59/65] done1 --- .../AzureAppConfigurationOptions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 860891f1..2137b149 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -391,14 +391,14 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString } /// - /// Connect the provider to CDN endpoint. + /// Connect the provider to Azure Front Door endpoint. /// - /// The endpoint of the CDN instance to connect to. + /// The endpoint of the Azure Front Door CDN instance to connect to. public AzureAppConfigurationOptions ConnectAzureFrontDoor(Uri endpoint) { if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) { - throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); + throw new InvalidOperationException("Cannot connect to both Azure App Configuration and Azure Front Door at the same time."); } if (endpoint == null) From c1d15aac3fa01624120d9c2f977baafeca4f3a23 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 12 Jun 2025 14:00:07 -0700 Subject: [PATCH 60/65] done2 --- .../AfdConfigurationClientManager.cs} | 6 +- .../{Cdn/CdnPolicy.cs => Afd/AfdPolicy.cs} | 30 ++-- .../AfdTokenAccessor.cs} | 10 +- .../{Cdn => Afd}/EmptyTokenCredential.cs | 2 +- .../IAfdTokenAccessor.cs} | 10 +- .../AzureAppConfigurationOptions.cs | 24 ++-- .../AzureAppConfigurationProvider.cs | 56 ++++---- .../AzureAppConfigurationSource.cs | 14 +- .../Constants/RequestTracingConstants.cs | 2 +- .../Extensions/PageExtensions.cs | 2 +- .../KeyValueChange.cs | 2 +- .../RequestTracingOptions.cs | 10 +- .../RefreshTests.cs | 0 .../Tests.AzureAppConfiguration/TestHelper.cs | 0 .../Unit/{CdnTests.cs => AfdTests.cs} | 130 +++++++++--------- .../Unit/TestHelper.cs | 2 +- 16 files changed, 150 insertions(+), 150 deletions(-) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{Cdn/CdnConfigurationClientManager.cs => Afd/AfdConfigurationClientManager.cs} (94%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{Cdn/CdnPolicy.cs => Afd/AfdPolicy.cs} (74%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{Cdn/CdnTokenAccessor.cs => Afd/AfdTokenAccessor.cs} (66%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{Cdn => Afd}/EmptyTokenCredential.cs (99%) rename src/Microsoft.Extensions.Configuration.AzureAppConfiguration/{Cdn/ICdnTokenAccessor.cs => Afd/IAfdTokenAccessor.cs} (66%) create mode 100644 tests/Tests.AzureAppConfiguration/RefreshTests.cs create mode 100644 tests/Tests.AzureAppConfiguration/TestHelper.cs rename tests/Tests.AzureAppConfiguration/Unit/{CdnTests.cs => AfdTests.cs} (80%) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs similarity index 94% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs index 3cebe84a..3f6ffc47 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs @@ -5,13 +5,13 @@ using Microsoft.Extensions.Azure; using System; using System.Collections.Generic; -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd { - internal class CdnConfigurationClientManager : IConfigurationClientManager + internal class AfdConfigurationClientManager : IConfigurationClientManager { private readonly ConfigurationClientWrapper _clientWrapper; - public CdnConfigurationClientManager( + public AfdConfigurationClientManager( IAzureClientFactory clientFactory, Uri endpoint) { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs similarity index 74% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs index a0c24f4a..2066d840 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnPolicy.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs @@ -7,29 +7,29 @@ using System.Diagnostics; using System.Web; -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd { /// - /// HTTP pipeline policy that injects current token into the query string for CDN cache breakage and consistency. - /// The injected token ensures CDN cache invalidation when configuration changes are detected and maintaining eventual consistency across distributed instances. + /// HTTP pipeline policy that injects current token into the query string for AFD cache breakage and consistency. + /// The injected token ensures AFD cache invalidation when configuration changes are detected and maintaining eventual consistency across distributed instances. /// - internal class CdnPolicy : HttpPipelinePolicy + internal class AfdPolicy : HttpPipelinePolicy { - private const string CdnTokenQueryParameter = "_"; + private const string AfdTokenQueryParameter = "_"; - private readonly ICdnTokenAccessor _cdnTokenAccessor; + private readonly IAfdTokenAccessor _afdTokenAccessor; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The token accessor that provides current token to be used for CDN cache breakage/consistency. - public CdnPolicy(ICdnTokenAccessor cdnTokenAccessor) + /// The token accessor that provides current token to be used for AFD cache breakage/consistency. + public AfdPolicy(IAfdTokenAccessor afdTokenAccessor) { - _cdnTokenAccessor = cdnTokenAccessor ?? throw new ArgumentNullException(nameof(cdnTokenAccessor)); + _afdTokenAccessor = afdTokenAccessor ?? throw new ArgumentNullException(nameof(afdTokenAccessor)); } /// - /// Processes the HTTP message and injects token into query string to break CDN cache when changes are detected. + /// Processes the HTTP message and injects token into query string to break AFD cache when changes are detected. /// This ensures fresh configuration data is retrieved when sentinel keys or collections have been modified. /// It also maintains eventual consistency across distributed instances by ensuring that the same token is used for all subsequent watch requests, until a new change is detected. /// @@ -37,7 +37,7 @@ public CdnPolicy(ICdnTokenAccessor cdnTokenAccessor) /// The pipeline. public override void Process(HttpMessage message, ReadOnlyMemory pipeline) { - string token = _cdnTokenAccessor.Current; + string token = _afdTokenAccessor.Current; if (!string.IsNullOrEmpty(token)) { message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); @@ -47,7 +47,7 @@ public override void Process(HttpMessage message, ReadOnlyMemory - /// Processes the HTTP message asynchronously and injects token into query string to break CDN cache when changes are detected. + /// Processes the HTTP message asynchronously and injects token into query string to break AFD cache when changes are detected. /// This ensures fresh configuration data is retrieved when sentinel keys or collections have been modified. /// It also maintains eventual consistency across distributed instances by ensuring that the same token is used for all subsequent watch requests, until a new change is detected. /// @@ -56,7 +56,7 @@ public override void Process(HttpMessage message, ReadOnlyMemoryA task representing the asynchronous operation. public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) { - string token = _cdnTokenAccessor.Current; + string token = _afdTokenAccessor.Current; if (!string.IsNullOrEmpty(token)) { message.Request.Uri.Reset(AddTokenToUri(message.Request.Uri.ToUri(), token)); @@ -72,7 +72,7 @@ private static Uri AddTokenToUri(Uri uri, string token) var uriBuilder = new UriBuilder(uri); var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query[CdnTokenQueryParameter] = token; + query[AfdTokenQueryParameter] = token; uriBuilder.Query = query.ToString(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnTokenAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdTokenAccessor.cs similarity index 66% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnTokenAccessor.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdTokenAccessor.cs index f4d30abc..732f9000 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/CdnTokenAccessor.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdTokenAccessor.cs @@ -1,18 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd { /// - /// Implementation of ICdnTokenAccessor that manages the current token for CDN cache breakage/consistency. + /// Implementation of IAfdTokenAccessor that manages the current token for AFD cache breakage/consistency. /// - internal class CdnTokenAccessor : ICdnTokenAccessor + internal class AfdTokenAccessor : IAfdTokenAccessor { private string _currentToken; /// - /// Gets or sets the current token value to be used for CDN cache breakage/consistency. - /// When null, CDN cache breakage/consistency is disabled. When not null, the token will be injected into requests. + /// Gets or sets the current token value to be used for AFD cache breakage/consistency. + /// When null, AFD cache breakage/consistency is disabled. When not null, the token will be injected into requests. /// public string Current { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/EmptyTokenCredential.cs similarity index 99% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/EmptyTokenCredential.cs index 0fb835e0..1239676f 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/EmptyTokenCredential.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/EmptyTokenCredential.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd { /// /// A token credential that provides an empty token. diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnTokenAccessor.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/IAfdTokenAccessor.cs similarity index 66% rename from src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnTokenAccessor.cs rename to src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/IAfdTokenAccessor.cs index e78fd7cb..aa85dcf0 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Cdn/ICdnTokenAccessor.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/IAfdTokenAccessor.cs @@ -1,16 +1,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. // -namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn +namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd { // - // Interface for accessing the CDN cache breakage/consistency token. + // Interface for accessing the AFD cache breakage/consistency token. // - internal interface ICdnTokenAccessor + internal interface IAfdTokenAccessor { /// - /// Gets or sets the current token value to be used for CDN cache breakage/consistency. - /// When null, CDN cache breakage/consistency is disabled. When not null, the token will be injected into requests. + /// Gets or sets the current token value to be used for AFD cache breakage/consistency. + /// When null, AFD cache breakage/consistency is disabled. When not null, the token will be injected into requests. /// string Current { get; set; } } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 2137b149..ee910763 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -5,7 +5,7 @@ using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault; -using Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions; using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement; using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models; @@ -156,14 +156,14 @@ internal IEnumerable Adapters internal IAzureClientFactory ClientFactory { get; private set; } /// - /// An accessor for current token to be used for CDN cache breakage/consistency. + /// An accessor for current token to be used for AFD cache breakage/consistency. /// - internal ICdnTokenAccessor CdnTokenAccessor { get; set; } + internal IAfdTokenAccessor AfdTokenAccessor { get; set; } /// - /// Gets a value indicating whether CDN is enabled. + /// Gets a value indicating whether AFD is enabled. /// - internal bool IsCdnEnabled { get; private set; } + internal bool IsAfdEnabled { get; private set; } /// /// Initializes a new instance of the class. @@ -369,9 +369,9 @@ public AzureAppConfigurationOptions Connect(string connectionString) /// public AzureAppConfigurationOptions Connect(IEnumerable connectionStrings) { - if (IsCdnEnabled) + if (IsAfdEnabled) { - throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); + throw new InvalidOperationException("Cannot connect to both Azure App Configuration and AFD at the same time."); } if (connectionStrings == null || !connectionStrings.Any()) @@ -393,7 +393,7 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// /// Connect the provider to Azure Front Door endpoint. /// - /// The endpoint of the Azure Front Door CDN instance to connect to. + /// The endpoint of the Azure Front Door AFD instance to connect to. public AzureAppConfigurationOptions ConnectAzureFrontDoor(Uri endpoint) { if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) @@ -411,8 +411,8 @@ public AzureAppConfigurationOptions ConnectAzureFrontDoor(Uri endpoint) Endpoints = new List() { endpoint }; ConnectionStrings = null; - IsCdnEnabled = true; - CdnTokenAccessor = new CdnTokenAccessor(); + IsAfdEnabled = true; + AfdTokenAccessor = new AfdTokenAccessor(); return this; } @@ -444,9 +444,9 @@ public AzureAppConfigurationOptions Connect(Uri endpoint, TokenCredential creden /// Token credential to use to connect. public AzureAppConfigurationOptions Connect(IEnumerable endpoints, TokenCredential credential) { - if (IsCdnEnabled) + if (IsAfdEnabled) { - throw new InvalidOperationException("Cannot connect to both Azure App Configuration and CDN at the same time."); + throw new InvalidOperationException("Cannot connect to both Azure App Configuration and AFD at the same time."); } if (endpoints == null || !endpoints.Any()) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs index 033fa893..24d6e96c 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationProvider.cs @@ -41,9 +41,9 @@ internal class AzureAppConfigurationProvider : ConfigurationProvider, IConfigura private Dictionary _configClientBackoffs = new Dictionary(); private DateTimeOffset _nextCollectionRefreshTime; - #region Cdn - private string _configCdnToken = null; - private string _ffCdnToken = null; + #region Afd + private string _configAfdToken = null; + private string _ffAfdToken = null; #endregion private readonly TimeSpan MinRefreshInterval; @@ -314,9 +314,9 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) => logInfoBuilder.Clear(); Uri endpoint = _configClientManager.GetEndpointForClient(client); - if (_options.IsCdnEnabled) + if (_options.IsAfdEnabled) { - _options.CdnTokenAccessor.Current = _configCdnToken; + _options.AfdTokenAccessor.Current = _configAfdToken; } if (_options.RegisterAllEnabled) @@ -335,7 +335,7 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa selector, matchConditions, _options.ConfigurationSettingPageIterator, - makeConditionalRequest: !_options.IsCdnEnabled, + makeConditionalRequest: !_options.IsAfdEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } @@ -343,17 +343,17 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa { refreshAll = true; - if (_options.IsCdnEnabled) + if (_options.IsAfdEnabled) { // - // Break cdn cache - string token = changedPage.GetCdnToken(); - _options.CdnTokenAccessor.Current = token; + // Break afd cache + string token = changedPage.GetAfdToken(); + _options.AfdTokenAccessor.Current = token; // // Reset versions so that next watch request will not use stale versions. - _configCdnToken = token; - _ffCdnToken = token; + _configAfdToken = token; + _ffAfdToken = token; } break; @@ -386,17 +386,17 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa { refreshAll = true; - if (_options.IsCdnEnabled) + if (_options.IsAfdEnabled) { // - // Break cdn cache - string token = change.GetCdnToken(); - _options.CdnTokenAccessor.Current = token; + // Break afd cache + string token = change.GetAfdToken(); + _options.AfdTokenAccessor.Current = token; // // Reset versions so that next watch request will not use stale versions. - _configCdnToken = token; - _ffCdnToken = token; + _configAfdToken = token; + _ffAfdToken = token; } break; @@ -419,9 +419,9 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa } // Get feature flag changes - if (_options.IsCdnEnabled) + if (_options.IsAfdEnabled) { - _options.CdnTokenAccessor.Current = _ffCdnToken; + _options.AfdTokenAccessor.Current = _ffAfdToken; } var ffSelectors = refreshableFfWatchers.Select(watcher => new KeyValueSelector @@ -442,7 +442,7 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa selector, matchConditions, _options.ConfigurationSettingPageIterator, - makeConditionalRequest: !_options.IsCdnEnabled, + makeConditionalRequest: !_options.IsAfdEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } @@ -450,16 +450,16 @@ await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Wa { ffCollectionUpdated = true; - if (_options.IsCdnEnabled) + if (_options.IsAfdEnabled) { // - // Break cdn cache - string token = changedPage.GetCdnToken(); - _options.CdnTokenAccessor.Current = token; + // Break afd cache + string token = changedPage.GetAfdToken(); + _options.AfdTokenAccessor.Current = token; // // Reset ff collection version so that next ff watch request will not use stale version. - _ffCdnToken = token; + _ffAfdToken = token; } break; @@ -1092,7 +1092,7 @@ private async Task CheckForChange(ConfigurationClient client, Ke if (_watchedIndividualKvs.TryGetValue(new KeyValueIdentifier(kvWatcher.Key, kvWatcher.Label), out ConfigurationSetting watchedKv)) { await TracingUtils.CallWithRequestTracing(_requestTracingEnabled, RequestType.Watch, _requestTracingOptions, - async () => change = await client.GetKeyValueChange(watchedKv, makeConditionalRequest: !_options.IsCdnEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + async () => change = await client.GetKeyValueChange(watchedKv, makeConditionalRequest: !_options.IsAfdEnabled, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); } else { @@ -1177,7 +1177,7 @@ private void SetRequestTracingOptions() IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured, FeatureFlagTracing = _options.FeatureFlagTracing, IsLoadBalancingEnabled = _options.LoadBalancingEnabled, - IsCdnEnabled = _options.IsCdnEnabled + IsAfdEnabled = _options.IsAfdEnabled }; } diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index dc614a51..ad4e809b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -4,7 +4,7 @@ using Azure.Core; using Azure.Data.AppConfiguration; using Microsoft.Extensions.Azure; -using Microsoft.Extensions.Configuration.AzureAppConfiguration.Cdn; +using Microsoft.Extensions.Configuration.AzureAppConfiguration.Afd; using System; using System.Collections.Generic; using System.Linq; @@ -38,19 +38,19 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) IAzureClientFactory clientFactory = options.ClientFactory; - if (options.IsCdnEnabled) + if (options.IsAfdEnabled) { if (options.LoadBalancingEnabled) { - throw new InvalidOperationException("Load balancing is not supported for CDN endpoint."); + throw new InvalidOperationException("Load balancing is not supported for AFD endpoint."); } if (clientFactory != null) { - throw new InvalidOperationException($"Custom client factory is not supported when connecting to CDN."); + throw new InvalidOperationException($"Custom client factory is not supported when connecting to AFD."); } - options.ClientOptions.AddPolicy(new CdnPolicy(options.CdnTokenAccessor), HttpPipelinePosition.PerCall); + options.ClientOptions.AddPolicy(new AfdPolicy(options.AfdTokenAccessor), HttpPipelinePosition.PerCall); } if (options.ClientManager != null) @@ -77,9 +77,9 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) throw new ArgumentException($"Please call {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.Connect)} or {nameof(AzureAppConfigurationOptions)}.{nameof(AzureAppConfigurationOptions.ConnectAzureFrontDoor)} to specify how to connect to Azure App Configuration."); } - if (options.IsCdnEnabled) + if (options.IsAfdEnabled) { - provider = new AzureAppConfigurationProvider(new CdnConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); + provider = new AzureAppConfigurationProvider(new AfdConfigurationClientManager(clientFactory, endpoints.First()), options, _optional); } else { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs index 5c007f74..c23774d8 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Constants/RequestTracingConstants.cs @@ -37,7 +37,7 @@ internal class RequestTracingConstants public const string SignalRUsedTag = "SignalR"; public const string FailoverRequestTag = "Failover"; public const string PushRefreshTag = "PushRefresh"; - public const string CdnTag = "CDN"; + public const string AfdTag = "AFD"; public const string FeatureFlagFilterTypeKey = "Filter"; public const string CustomFilter = "CSTM"; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs index bdb568fe..c12796ac 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/PageExtensions.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions { internal static class PageExtensions { - public static string GetCdnToken(this Page page) + public static string GetAfdToken(this Page page) { using Response response = page.GetRawResponse(); diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs index 3cbf1863..b1909be6 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/KeyValueChange.cs @@ -27,7 +27,7 @@ internal struct KeyValueChange public ConfigurationSetting Previous { get; set; } - public string GetCdnToken() + public string GetAfdToken() { string token; diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs index 819f8397..96c9b843 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/RequestTracingOptions.cs @@ -71,9 +71,9 @@ internal class RequestTracingOptions public bool IsPushRefreshUsed { get; set; } = false; /// - /// Flag to indicate wether the request is sent to a CDN. + /// Flag to indicate wether the request is sent to a AFD. /// - public bool IsCdnEnabled { get; set; } = false; + public bool IsAfdEnabled { get; set; } = false; /// /// Flag to indicate whether any key-value uses the json content type and contains @@ -126,7 +126,7 @@ public bool UsesAnyTracingFeature() IsSignalRUsed || UsesAIConfiguration || UsesAIChatCompletionConfiguration || - IsCdnEnabled; + IsAfdEnabled; } /// @@ -177,14 +177,14 @@ public string CreateFeaturesString() sb.Append(RequestTracingConstants.AIChatCompletionConfigurationTag); } - if (IsCdnEnabled) + if (IsAfdEnabled) { if (sb.Length > 0) { sb.Append(RequestTracingConstants.Delimiter); } - sb.Append(RequestTracingConstants.CdnTag); + sb.Append(RequestTracingConstants.AfdTag); } return sb.ToString(); diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs new file mode 100644 index 00000000..e69de29b diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs new file mode 100644 index 00000000..e69de29b diff --git a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs b/tests/Tests.AzureAppConfiguration/Unit/AfdTests.cs similarity index 80% rename from tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs rename to tests/Tests.AzureAppConfiguration/Unit/AfdTests.cs index 5dc58773..070e3c0d 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/CdnTests.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/AfdTests.cs @@ -17,7 +17,7 @@ namespace Tests.AzureAppConfiguration { - public class CdnTests + public class AfdTests { List _kvCollection = new List { @@ -58,7 +58,7 @@ public class CdnTests }; [Fact] - public void CdnTests_DoesNotSupportCustomClientFactory() + public void AfdTests_DoesNotSupportCustomClientFactory() { var mockClientFactory = new Mock>(); @@ -66,7 +66,7 @@ public void CdnTests_DoesNotSupportCustomClientFactory() .AddAzureAppConfiguration(options => { options.SetClientFactory(mockClientFactory.Object) - .ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint); + .ConnectAzureFrontDoor(TestHelpers.MockAfdEndpoint); }); Exception exception = Assert.Throws(() => configBuilder.Build()); @@ -74,12 +74,12 @@ public void CdnTests_DoesNotSupportCustomClientFactory() } [Fact] - public void CdnTests_DoesNotSupportLoadBalancing() + public void AfdTests_DoesNotSupportLoadBalancing() { var configBuilder = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockAfdEndpoint) .LoadBalancingEnabled = true; }); @@ -88,7 +88,7 @@ public void CdnTests_DoesNotSupportLoadBalancing() } [Fact] - public async Task CdnTests_RefreshWithRegisterAll() + public async Task AfdTests_RefreshWithRegisterAll() { var keyValueCollection = new List(_kvCollection); var mockResponse = new Mock(); @@ -104,7 +104,7 @@ public async Task CdnTests_RefreshWithRegisterAll() var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockAfdEndpoint) .Select("TestKey*") .ConfigureRefresh(refreshOptions => { @@ -123,11 +123,11 @@ public async Task CdnTests_RefreshWithRegisterAll() Assert.Equal("TestValue2", config["TestKey2"]); Assert.Equal("TestValue3", config["TestKey3"]); - // Verify CDN is enabled - Assert.True(capturedOptions.IsCdnEnabled); + // Verify AFD is enabled + Assert.True(capturedOptions.IsAfdEnabled); - // Verify that current CDN token is null at startup - Assert.Null(capturedOptions.CdnTokenAccessor.Current); + // Verify that current AFD token is null at startup + Assert.Null(capturedOptions.AfdTokenAccessor.Current); // // change @@ -139,18 +139,18 @@ public async Task CdnTests_RefreshWithRegisterAll() // Wait for the cache to expire await Task.Delay(1500); - // Trigger refresh - this should set a token in the CDN token accessor + // Trigger refresh - this should set a token in the AFD token accessor await refresher.RefreshAsync(); - // Verify that the CDN token accessor has a token set to new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + // Verify that the AFD token accessor has a token set to new value + Assert.NotNull(capturedOptions.AfdTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.AfdTokenAccessor.Current); // Verify the configuration was updated Assert.Equal("newValue", config["TestKey1"]); } - string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; + string previousAfdToken = capturedOptions.AfdTokenAccessor.Current; // // no change @@ -160,8 +160,8 @@ public async Task CdnTests_RefreshWithRegisterAll() await refresher.RefreshAsync(); - // Verify that the CDN token accessor has a token set to previous CDN token - Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + // Verify that the AFD token accessor has a token set to previous AFD token + Assert.Equal(previousAfdToken, capturedOptions.AfdTokenAccessor.Current); } // @@ -174,13 +174,13 @@ public async Task CdnTests_RefreshWithRegisterAll() // Wait for the cache to expire await Task.Delay(1500); - // Trigger refresh - this should set a token in the CDN token accessor + // Trigger refresh - this should set a token in the AFD token accessor await refresher.RefreshAsync(); - // Verify that the CDN token accessor has a token set to new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + // Verify that the AFD token accessor has a token set to new value + Assert.NotNull(capturedOptions.AfdTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.AfdTokenAccessor.Current); + Assert.NotEqual(previousAfdToken, capturedOptions.AfdTokenAccessor.Current); // Verify the configuration was updated Assert.Equal("anotherNewValue", config["TestKey1"]); @@ -188,7 +188,7 @@ public async Task CdnTests_RefreshWithRegisterAll() } [Fact] - public async Task CdnTests_RefreshWithRegister() + public async Task AfdTests_RefreshWithRegister() { var keyValueCollection = new List(_kvCollection); var mockResponse = new Mock(); @@ -235,7 +235,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool _ var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockAfdEndpoint) .Select("TestKey*") .ConfigureRefresh(refreshOptions => { @@ -254,11 +254,11 @@ Response GetIfChanged(ConfigurationSetting setting, bool _ Assert.Equal("TestValue2", config["TestKey2"]); Assert.Equal("TestValue3", config["TestKey3"]); - // Verify CDN is enabled - Assert.True(capturedOptions.IsCdnEnabled); + // Verify AFD is enabled + Assert.True(capturedOptions.IsAfdEnabled); - // Verify that current CDN token is null at startup - Assert.Null(capturedOptions.CdnTokenAccessor.Current); + // Verify that current AFD token is null at startup + Assert.Null(capturedOptions.AfdTokenAccessor.Current); // // change @@ -268,18 +268,18 @@ Response GetIfChanged(ConfigurationSetting setting, bool _ // Wait for the cache to expire await Task.Delay(1500); - // Trigger refresh - this should set a token in the CDN token accessor + // Trigger refresh - this should set a token in the AFD token accessor await refresher.RefreshAsync(); - // Verify that the CDN token is set to the new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); + // Verify that the AFD token is set to the new value + Assert.NotNull(capturedOptions.AfdTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.AfdTokenAccessor.Current); // Verify the configuration was updated Assert.Equal("newValue", config["TestKey1"]); } - string previousCdnToken = capturedOptions.CdnTokenAccessor.Current; + string previousAfdToken = capturedOptions.AfdTokenAccessor.Current; // // no change @@ -288,8 +288,8 @@ Response GetIfChanged(ConfigurationSetting setting, bool _ await refresher.RefreshAsync(); - // Verify that the CDN token accessor has a token set to previous CDN token - Assert.Equal(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + // Verify that the AFD token accessor has a token set to previous AFD token + Assert.Equal(previousAfdToken, capturedOptions.AfdTokenAccessor.Current); } // @@ -300,13 +300,13 @@ Response GetIfChanged(ConfigurationSetting setting, bool _ // Wait for the cache to expire await Task.Delay(1500); - // Trigger refresh - this should set a token in the CDN token accessor + // Trigger refresh - this should set a token in the AFD token accessor await refresher.RefreshAsync(); - // Verify that the CDN token accessor has a token set to new value - Assert.NotNull(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEmpty(capturedOptions.CdnTokenAccessor.Current); - Assert.NotEqual(previousCdnToken, capturedOptions.CdnTokenAccessor.Current); + // Verify that the AFD token accessor has a token set to new value + Assert.NotNull(capturedOptions.AfdTokenAccessor.Current); + Assert.NotEmpty(capturedOptions.AfdTokenAccessor.Current); + Assert.NotEqual(previousAfdToken, capturedOptions.AfdTokenAccessor.Current); // Verify the configuration was updated Assert.Null(config["TestKey1"]); @@ -314,7 +314,7 @@ Response GetIfChanged(ConfigurationSetting setting, bool _ } [Fact] - public async Task CdnTests_ParallelAppsHaveSameCdnTokenSequence() + public async Task AfdTests_ParallelAppsHaveSameAfdTokenSequence() { var mockAsyncPageable = new MockAsyncPageable(_kvCollection.ToList()); @@ -327,7 +327,7 @@ public async Task CdnTests_ParallelAppsHaveSameCdnTokenSequence() var noChangeGate = new SemaphoreSlim(0, 2); var secondChangeGate = new SemaphoreSlim(0, 2); - async Task CreateAppTask(List cdnTokenList) + async Task CreateAppTask(List afdTokenList) { var mockClient = new Mock(MockBehavior.Strict); @@ -341,7 +341,7 @@ async Task CreateAppTask(List cdnTokenList) var config = new ConfigurationBuilder() .AddAzureAppConfiguration(options => { - options.ConnectAzureFrontDoor(TestHelpers.MockCdnEndpoint) + options.ConnectAzureFrontDoor(TestHelpers.MockAfdEndpoint) .Select("TestKey*") .ConfigureRefresh(refreshOptions => { @@ -356,8 +356,8 @@ async Task CreateAppTask(List cdnTokenList) }) .Build(); - // Initial state - CDN token should be null - cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + // Initial state - AFD token should be null + afdTokenList.Add(capturedOptions.AfdTokenAccessor.Current); // Signal that this app is initialized startupSync.Release(); @@ -367,14 +367,14 @@ async Task CreateAppTask(List cdnTokenList) await Task.Delay(1500); await refresher.RefreshAsync(); - cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + afdTokenList.Add(capturedOptions.AfdTokenAccessor.Current); // No change (should keep same token) await noChangeGate.WaitAsync(); await Task.Delay(1500); await refresher.RefreshAsync(); - cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + afdTokenList.Add(capturedOptions.AfdTokenAccessor.Current); // Signal that this app is done with no-change refresh noChangeSync.Release(); @@ -384,7 +384,7 @@ async Task CreateAppTask(List cdnTokenList) await Task.Delay(1500); await refresher.RefreshAsync(); - cdnTokenList.Add(capturedOptions.CdnTokenAccessor.Current); + afdTokenList.Add(capturedOptions.AfdTokenAccessor.Current); } var changeTask = Task.Run(async () => @@ -410,21 +410,21 @@ async Task CreateAppTask(List cdnTokenList) }); // Run both apps in parallel along with the change coordinator - var app1CdnTokens = new List(); - var app2CdnTokens = new List(); - var task1 = CreateAppTask(app1CdnTokens); - var task2 = CreateAppTask(app2CdnTokens); + var app1AfdTokens = new List(); + var app2AfdTokens = new List(); + var task1 = CreateAppTask(app1AfdTokens); + var task2 = CreateAppTask(app2AfdTokens); await Task.WhenAll(task1, task2, changeTask); // Verify both apps captured the same number of tokens - Assert.Equal(4, app1CdnTokens.Count); - Assert.Equal(4, app2CdnTokens.Count); + Assert.Equal(4, app1AfdTokens.Count); + Assert.Equal(4, app2AfdTokens.Count); - // Verify the CDN token sequences are identical between the two apps - for (int i = 0; i < app1CdnTokens.Count; i++) + // Verify the AFD token sequences are identical between the two apps + for (int i = 0; i < app1AfdTokens.Count; i++) { - Assert.True(app1CdnTokens[i] == app2CdnTokens[i]); + Assert.True(app1AfdTokens[i] == app2AfdTokens[i]); } // Verify the expected token pattern: @@ -432,13 +432,13 @@ async Task CreateAppTask(List cdnTokenList) // Index 1: non-null (after first change) // Index 2: same as index 1 (no change) // Index 3: non-null and different from index 1 (after second change) - Assert.Null(app1CdnTokens[0]); - Assert.NotNull(app1CdnTokens[1]); - Assert.NotEmpty(app1CdnTokens[1]); - Assert.Equal(app1CdnTokens[1], app1CdnTokens[2]); - Assert.NotNull(app1CdnTokens[3]); - Assert.NotEmpty(app1CdnTokens[3]); - Assert.NotEqual(app1CdnTokens[1], app1CdnTokens[3]); + Assert.Null(app1AfdTokens[0]); + Assert.NotNull(app1AfdTokens[1]); + Assert.NotEmpty(app1AfdTokens[1]); + Assert.Equal(app1AfdTokens[1], app1AfdTokens[2]); + Assert.NotNull(app1AfdTokens[3]); + Assert.NotEmpty(app1AfdTokens[3]); + Assert.NotEqual(app1AfdTokens[1], app1AfdTokens[3]); } } } diff --git a/tests/Tests.AzureAppConfiguration/Unit/TestHelper.cs b/tests/Tests.AzureAppConfiguration/Unit/TestHelper.cs index bbb06e6e..53ff0162 100644 --- a/tests/Tests.AzureAppConfiguration/Unit/TestHelper.cs +++ b/tests/Tests.AzureAppConfiguration/Unit/TestHelper.cs @@ -72,7 +72,7 @@ static public string CreateMockEndpointString(string endpoint = "https://azure.a return $"Endpoint={endpoint};Id=b1d9b31;Secret={returnValue}"; } - static public Uri MockCdnEndpoint => new Uri("https://cdn.azurefd.net"); + static public Uri MockAfdEndpoint => new Uri("https://afd.azurefd.net"); static public void SerializeSetting(ref Utf8JsonWriter json, ConfigurationSetting setting) { From 0292b784181ff7833177b4dab7c6673fd347277b Mon Sep 17 00:00:00 2001 From: Sami Sadfa <71456174+samsadsam@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:38:15 -0700 Subject: [PATCH 61/65] Update src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs Co-authored-by: Jimmy Campbell --- .../AzureAppConfigurationSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index ad4e809b..704ea3ee 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -42,7 +42,7 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) { if (options.LoadBalancingEnabled) { - throw new InvalidOperationException("Load balancing is not supported for AFD endpoint."); + throw new InvalidOperationException("Load balancing is not supported when connecting to AFD."); } if (clientFactory != null) From be6d8883dec0c8220b29eb92d252052b3723b2b6 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 12 Jun 2025 15:38:44 -0700 Subject: [PATCH 62/65] done3 --- tests/Tests.AzureAppConfiguration/RefreshTests.cs | 0 tests/Tests.AzureAppConfiguration/TestHelper.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/Tests.AzureAppConfiguration/RefreshTests.cs delete mode 100644 tests/Tests.AzureAppConfiguration/TestHelper.cs diff --git a/tests/Tests.AzureAppConfiguration/RefreshTests.cs b/tests/Tests.AzureAppConfiguration/RefreshTests.cs deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/Tests.AzureAppConfiguration/TestHelper.cs b/tests/Tests.AzureAppConfiguration/TestHelper.cs deleted file mode 100644 index e69de29b..00000000 From eef8180fe556ed65b8d1b95b17b77d848a537d00 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Thu, 12 Jun 2025 15:40:38 -0700 Subject: [PATCH 63/65] donedone --- .../AzureAppConfigurationOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index ee910763..9af6fb23 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -393,7 +393,7 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// /// Connect the provider to Azure Front Door endpoint. /// - /// The endpoint of the Azure Front Door AFD instance to connect to. + /// The endpoint of the Azure Front Door (Afd) instance to connect to. public AzureAppConfigurationOptions ConnectAzureFrontDoor(Uri endpoint) { if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) From cae49ce99685ccb44075bb8d8f222696b99d5279 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Mon, 23 Jun 2025 15:57:45 -0700 Subject: [PATCH 64/65] donedonedone --- .../Afd/AfdConfigurationClientManager.cs | 2 +- .../AzureAppConfigurationOptions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs index 3f6ffc47..87c8c24b 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdConfigurationClientManager.cs @@ -30,7 +30,7 @@ public AfdConfigurationClientManager( public IEnumerable GetClients() { - return new[] { _clientWrapper.Client }; + return new List { _clientWrapper.Client }; } public void RefreshClients() diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs index 9af6fb23..1fe7dfbf 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs @@ -393,7 +393,7 @@ public AzureAppConfigurationOptions Connect(IEnumerable connectionString /// /// Connect the provider to Azure Front Door endpoint. /// - /// The endpoint of the Azure Front Door (Afd) instance to connect to. + /// The endpoint of the Azure Front Door (AFD) instance to connect to. public AzureAppConfigurationOptions ConnectAzureFrontDoor(Uri endpoint) { if ((Credential != null && !(Credential is EmptyTokenCredential)) || (ConnectionStrings?.Any() ?? false)) From 39ddcae56d66e8adab3f910d58a9e947629e2060 Mon Sep 17 00:00:00 2001 From: Sami Sadfa Date: Wed, 25 Jun 2025 15:52:46 -0700 Subject: [PATCH 65/65] remove auth header when connecting to cdn --- .../Afd/AfdPolicy.cs | 2 ++ .../AzureAppConfigurationSource.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs index 2066d840..c7293d41 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Afd/AfdPolicy.cs @@ -37,6 +37,7 @@ public AfdPolicy(IAfdTokenAccessor afdTokenAccessor) /// The pipeline. public override void Process(HttpMessage message, ReadOnlyMemory pipeline) { + message.Request.Headers.Remove("Authorization"); string token = _afdTokenAccessor.Current; if (!string.IsNullOrEmpty(token)) { @@ -56,6 +57,7 @@ public override void Process(HttpMessage message, ReadOnlyMemoryA task representing the asynchronous operation. public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory pipeline) { + message.Request.Headers.Remove("Authorization"); string token = _afdTokenAccessor.Current; if (!string.IsNullOrEmpty(token)) { diff --git a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs index 704ea3ee..7e7bcc20 100644 --- a/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationSource.cs @@ -50,7 +50,7 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) throw new InvalidOperationException($"Custom client factory is not supported when connecting to AFD."); } - options.ClientOptions.AddPolicy(new AfdPolicy(options.AfdTokenAccessor), HttpPipelinePosition.PerCall); + options.ClientOptions.AddPolicy(new AfdPolicy(options.AfdTokenAccessor), HttpPipelinePosition.PerRetry); } if (options.ClientManager != null)