Skip to content

add connect Cdn API #601

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: preview
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ internal IEnumerable<IKeyValueAdapter> Adapters
/// </summary>
internal IAzureClientFactory<ConfigurationClient> ClientFactory { get; private set; }

/// <summary>
/// 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.
/// </summary>
internal ICdnCacheBustingAccessor CdnCacheBustingAccessor { get; private set; }

/// <summary>
/// Initializes a new instance of the <see cref="AzureAppConfigurationOptions"/> class.
/// </summary>
Expand Down Expand Up @@ -339,6 +345,11 @@ public AzureAppConfigurationOptions Connect(string connectionString)
/// </param>
public AzureAppConfigurationOptions Connect(IEnumerable<string> 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));
Expand All @@ -355,6 +366,30 @@ public AzureAppConfigurationOptions Connect(IEnumerable<string> connectionString
return this;
}

/// <summary>
/// Connect the provider to CDN endpoint.
/// </summary>
/// <param name="endpoint">The endpoint of the CDN instance to connect to.</param>
public AzureAppConfigurationOptions ConnectCdn(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.");
}

if (endpoint == null)
{
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<Uri>() { endpoint }, new EmptyTokenCredential());
}

/// <summary>
/// Connect the provider to Azure App Configuration using endpoint and token credentials.
/// </summary>
Expand Down Expand Up @@ -382,6 +417,11 @@ public AzureAppConfigurationOptions Connect(Uri endpoint, TokenCredential creden
/// <param name="credential">Token credential to use to connect.</param>
public AzureAppConfigurationOptions Connect(IEnumerable<Uri> 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,8 @@ private async Task<bool> RefreshIndividualKvWatchers(
StringBuilder logInfoBuilder,
CancellationToken cancellationToken)
{
bool cdnMode = _options.CdnCacheBustingAccessor != null;

foreach (KeyValueWatcher kvWatcher in refreshableIndividualKvWatchers)
{
string watchedKey = kvWatcher.Key;
Expand All @@ -962,8 +964,13 @@ private async Task<bool> 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
{
Expand Down Expand Up @@ -1000,6 +1007,11 @@ await CallWithRequestTracing(

if (kvWatcher.RefreshAll)
{
if (cdnMode)
{
_options.CdnCacheBustingAccessor.CurrentETag = change.Current.ETag.ToString();
}

return true;
}
}
Expand All @@ -1009,6 +1021,11 @@ await CallWithRequestTracing(
}
}

if (cdnMode)
{
_options.CdnCacheBustingAccessor.CurrentETag = null;
}

return false;
}

Expand Down Expand Up @@ -1065,7 +1082,8 @@ private void SetRequestTracingOptions()
IsKeyVaultConfigured = _options.IsKeyVaultConfigured,
IsKeyVaultRefreshConfigured = _options.IsKeyVaultRefreshConfigured,
FeatureFlagTracing = _options.FeatureFlagTracing,
IsLoadBalancingEnabled = _options.LoadBalancingEnabled
IsLoadBalancingEnabled = _options.LoadBalancingEnabled,
IsCdnUsed = _options.Credential is EmptyTokenCredential
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System.Threading;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
/// <summary>
/// Implementation of ICdnCacheBustingAccessor that uses AsyncLocal for thread-safe context management.
/// </summary>
internal class CdnCacheBustingAccessor : ICdnCacheBustingAccessor
{
private static readonly AsyncLocal<CdnCacheBustingContext> _context = new AsyncLocal<CdnCacheBustingContext>();

/// <summary>
/// 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.
/// </summary>
public string CurrentETag
{
get => _context.Value?.ETag;
set => EnsureContext().ETag = value;
}

private static CdnCacheBustingContext EnsureContext()
{
return _context.Value ??= new CdnCacheBustingContext();
}
}

/// <summary>
/// Context class that holds the CDN cache busting state.
/// </summary>
internal class CdnCacheBustingContext
{
/// <summary>
/// Gets or sets the ETag value for cache busting.
/// </summary>
public string ETag { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// HTTP pipeline policy that injects ETags into the query string for CDN cache busting.
/// </summary>
internal class CdnCacheBustingPolicy : HttpPipelinePolicy
{
private readonly ICdnCacheBustingAccessor _cacheBustingAccessor;

/// <summary>
/// Initializes a new instance of the <see cref="CdnCacheBustingPolicy"/> class.
/// </summary>
/// <param name="cacheBustingAccessor">The CDN cache busting accessor.</param>
public CdnCacheBustingPolicy(ICdnCacheBustingAccessor cacheBustingAccessor)
{
_cacheBustingAccessor = cacheBustingAccessor ?? throw new ArgumentNullException(nameof(cacheBustingAccessor));
}

/// <summary>
/// Processes the HTTP message and injects ETag into query string if CDN cache busting is enabled.
/// </summary>
/// <param name="message">The HTTP message.</param>
/// <param name="pipeline">The pipeline.</param>
public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> 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);
}

/// <summary>
/// Processes the HTTP message asynchronously and injects ETag into query string if CDN cache busting is enabled.
/// </summary>
/// <param name="message">The HTTP message.</param>
/// <param name="pipeline">The pipeline.</param>
/// <returns>A task representing the asynchronous operation.</returns>
public override async System.Threading.Tasks.ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Azure;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
internal class CdnConfigurationClientManager : IConfigurationClientManager
{
private readonly IList<ConfigurationClientWrapper> _clients;

public CdnConfigurationClientManager(
IAzureClientFactory<ConfigurationClient> clientFactory,
IEnumerable<Uri> endpoints)
{
if (clientFactory == null)
{
throw new ArgumentNullException(nameof(clientFactory));
}

_clients = endpoints
.Select(endpoint => new ConfigurationClientWrapper(endpoint, clientFactory.CreateClient(endpoint.AbsoluteUri)))
.ToList();
}

public IEnumerable<ConfigurationClient> GetClients()
{
return _clients.Select(c => c.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));
}

ConfigurationClientWrapper clientWrapper = _clients.SingleOrDefault(c => new EndpointComparer().Equals(c.Endpoint, endpoint));

if (clientWrapper != null)
{
clientWrapper.Client.UpdateSyncToken(syncToken);

return true;
}

return false;
}

public Uri GetEndpointForClient(ConfigurationClient client)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}

ConfigurationClientWrapper currentClient = _clients.FirstOrDefault(c => c.Client == client);

return currentClient?.Endpoint;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +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 FeatureFlagFilterTypeKey = "Filter";
public const string CustomFilter = "CSTM";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Azure.Core;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
/// <summary>
/// A token credential that provides an empty token.
/// </summary>
internal class EmptyTokenCredential : TokenCredential
{
/// <summary>
/// Gets an empty token.
/// </summary>
/// <param name="requestContext">The context of the token request.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>An empty access token.</returns>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new AccessToken(string.Empty, DateTimeOffset.MaxValue);
}

/// <summary>
/// Asynchronously gets an empty token.
/// </summary>
/// <param name="requestContext">The context of the token request.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an empty access token.</returns>
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(new AccessToken(string.Empty, DateTimeOffset.MaxValue));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
{
internal static class ConfigurationClientExtensions
{
public static async Task<KeyValueChange> GetKeyValueChange(this ConfigurationClient client, ConfigurationSetting setting, CancellationToken cancellationToken)
public static async Task<KeyValueChange> GetKeyValueChange(this ConfigurationClient client, ConfigurationSetting setting, CancellationToken cancellationToken, bool makeConditionalRequest = true)
{
if (setting == null)
{
Expand All @@ -28,7 +28,7 @@ public static async Task<KeyValueChange> GetKeyValueChange(this ConfigurationCli

try
{
Response<ConfigurationSetting> response = await client.GetConfigurationSettingAsync(setting, onlyIfChanged: true, cancellationToken).ConfigureAwait(false);
Response<ConfigurationSetting> response = await client.GetConfigurationSettingAsync(setting, onlyIfChanged: makeConditionalRequest, cancellationToken).ConfigureAwait(false);
if (response.GetRawResponse().Status == (int)HttpStatusCode.OK &&
!response.Value.ETag.Equals(setting.ETag))
{
Expand Down
Loading