Skip to content

Merge main to release/stable/v8 #652

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

Merged
merged 9 commits into from
Apr 21, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<!-- Nuget Package Version Settings -->

<PropertyGroup>
<OfficialVersion>8.1.1</OfficialVersion>
<OfficialVersion>8.1.2</OfficialVersion>
</PropertyGroup>

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

<PropertyGroup>
<OfficialVersion>8.1.1</OfficialVersion>
<OfficialVersion>8.1.2</OfficialVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(CDP_PATCH_NUMBER)'!='' AND '$(CDP_BUILD_TYPE)'=='Official'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using System;
using System.Collections.Generic;
using System.Security;
Expand Down Expand Up @@ -99,11 +100,11 @@ public static IServiceCollection AddAzureAppConfiguration(this IServiceCollectio
if (!_isProviderDisabled)
{
services.AddLogging();
services.AddSingleton<IConfigurationRefresherProvider, AzureAppConfigurationRefresherProvider>();
services.TryAddSingleton<IConfigurationRefresherProvider, AzureAppConfigurationRefresherProvider>();
}
else
{
services.AddSingleton<IConfigurationRefresherProvider, EmptyConfigurationRefresherProvider>();
services.TryAddSingleton<IConfigurationRefresherProvider, EmptyConfigurationRefresherProvider>();
}

return services;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ public AzureAppConfigurationProvider(IConfigurationClientManager configClientMan

if (options.RegisterAllEnabled)
{
if (options.KvCollectionRefreshInterval <= TimeSpan.Zero)
{
throw new ArgumentException(
$"{nameof(options.KvCollectionRefreshInterval)} must be greater than zero seconds when using RegisterAll for refresh",
nameof(options));
}

MinRefreshInterval = TimeSpan.FromTicks(Math.Min(minWatcherRefreshInterval.Ticks, options.KvCollectionRefreshInterval.Ticks));
}
else if (hasWatchers)
Expand Down Expand Up @@ -206,7 +213,7 @@ public async Task RefreshAsync(CancellationToken cancellationToken)
var utcNow = DateTimeOffset.UtcNow;
IEnumerable<KeyValueWatcher> refreshableIndividualKvWatchers = _options.IndividualKvWatchers.Where(kvWatcher => utcNow >= kvWatcher.NextRefreshTime);
IEnumerable<KeyValueWatcher> refreshableFfWatchers = _options.FeatureFlagWatchers.Where(ffWatcher => utcNow >= ffWatcher.NextRefreshTime);
bool isRefreshDue = utcNow >= _nextCollectionRefreshTime;
bool isRefreshDue = _options.RegisterAllEnabled && utcNow >= _nextCollectionRefreshTime;

// Skip refresh if mappedData is loaded, but none of the watchers or adapters are refreshable.
if (_mappedData != null &&
Expand Down Expand Up @@ -412,7 +419,7 @@ await ExecuteWithFailOverPolicyAsync(clients, async (client) =>
}
}

if (isRefreshDue)
if (_options.RegisterAllEnabled && isRefreshDue)
{
_nextCollectionRefreshTime = DateTimeOffset.UtcNow.Add(_options.KvCollectionRefreshInterval);
}
Expand Down Expand Up @@ -590,9 +597,21 @@ private async Task<Dictionary<string, string>> PrepareData(Dictionary<string, Co
// Reset old feature flag tracing in order to track the information present in the current response from server.
_options.FeatureFlagTracing.ResetFeatureFlagTracing();

// Reset old request tracing values for content type
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.ResetAiConfigurationTracing();
}

foreach (KeyValuePair<string, ConfigurationSetting> kvp in data)
{
IEnumerable<KeyValuePair<string, string>> keyValuePairs = null;

if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.UpdateAiConfigurationTracing(kvp.Value.ContentType);
}

keyValuePairs = await ProcessAdapters(kvp.Value, cancellationToken).ConfigureAwait(false);

foreach (KeyValuePair<string, string> kv in keyValuePairs)
Expand Down Expand Up @@ -629,7 +648,7 @@ private async Task LoadAsync(bool ignoreFailures, CancellationToken cancellation
{
IEnumerable<ConfigurationClient> clients = _configClientManager.GetClients();

if (_requestTracingOptions != null)
if (_requestTracingEnabled && _requestTracingOptions != null)
{
_requestTracingOptions.ReplicaCount = clients.Count() - 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,38 @@ internal class AzureAppConfigurationRefresherProvider : IConfigurationRefresherP
{
private static readonly PropertyInfo _propertyInfo = typeof(ChainedConfigurationProvider).GetProperty("Configuration", BindingFlags.Public | BindingFlags.Instance);

public IEnumerable<IConfigurationRefresher> Refreshers { get; }
private readonly IConfiguration _configuration;
private readonly ILoggerFactory _loggerFactory;
private IEnumerable<IConfigurationRefresher> _refreshers;
private bool _rediscoveredRefreshers = false;

public AzureAppConfigurationRefresherProvider(IConfiguration configuration, ILoggerFactory _loggerFactory)
public IEnumerable<IConfigurationRefresher> Refreshers
{
var configurationRoot = configuration as IConfigurationRoot;
get
{
// Ensure latest refreshers are discovered if the configuration has changed since the constructor was called
if (!_rediscoveredRefreshers)
{
_refreshers = DiscoverRefreshers();

_rediscoveredRefreshers = true;
}

return _refreshers;
}
}

public AzureAppConfigurationRefresherProvider(IConfiguration configuration, ILoggerFactory loggerFactory)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
_loggerFactory = loggerFactory;

_refreshers = DiscoverRefreshers();
}

private IEnumerable<IConfigurationRefresher> DiscoverRefreshers()
{
var configurationRoot = _configuration as IConfigurationRoot;
var refreshers = new List<IConfigurationRefresher>();

FindRefreshers(configurationRoot, _loggerFactory, refreshers);
Expand All @@ -27,7 +54,7 @@ public AzureAppConfigurationRefresherProvider(IConfiguration configuration, ILog
throw new InvalidOperationException("Unable to access the Azure App Configuration provider. Please ensure that it has been configured correctly.");
}

Refreshers = refreshers;
return refreshers;
}

private void FindRefreshers(IConfigurationRoot configurationRoot, ILoggerFactory loggerFactory, List<IConfigurationRefresher> refreshers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
using Azure;
using Azure.Data.AppConfiguration;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -72,8 +75,15 @@ KeyVaultReferenceException CreateKeyVaultReferenceException(string message, Conf

public bool CanProcess(ConfigurationSetting setting)
{
string contentType = setting?.ContentType?.Split(';')[0].Trim();
return string.Equals(contentType, KeyVaultConstants.ContentType);
if (setting == null ||
string.IsNullOrWhiteSpace(setting.Value) ||
string.IsNullOrWhiteSpace(setting.ContentType))
{
return false;
}

return setting.ContentType.TryParseContentType(out ContentType contentType)
&& contentType.IsKeyVaultReference();
}

public void OnChangeDetected(ConfigurationSetting setting = null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Azure.Data.AppConfiguration;
using Azure;
using Azure;
using Azure.Data.AppConfiguration;
using System.Collections.Generic;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ internal class RequestTracingConstants
public const string ReplicaCountKey = "ReplicaCount";
public const string FeaturesKey = "Features";
public const string LoadBalancingEnabledTag = "LB";
public const string AIConfigurationTag = "AI";
public const string AIChatCompletionConfigurationTag = "AICC";

public const string SignalRUsedTag = "SignalR";
public const string FailoverRequestTag = "Failover";
public const string PushRefreshTag = "PushRefresh";
Expand All @@ -54,5 +57,8 @@ internal class RequestTracingConstants
public const string SignalRAssemblyName = "Microsoft.AspNetCore.SignalR";

public const string Delimiter = "+";

public const string AIMimeProfile = "https://azconfig.io/mime-profiles/ai";
public const string AIChatCompletionMimeProfile = "https://azconfig.io/mime-profiles/ai/chat-completion";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System.Linq;
using System;
using System.Net.Mime;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using System.Collections.Generic;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
{
internal static class ContentTypeExtensions
{
public static bool IsAi(this ContentType contentType)
{
return contentType != null &&
contentType.IsJson() &&
!contentType.IsFeatureFlag() &&
!contentType.IsKeyVaultReference() &&
contentType.Parameters.ContainsKey("profile") &&
!string.IsNullOrEmpty(contentType.Parameters["profile"]) &&
contentType.Parameters["profile"].StartsWith(RequestTracingConstants.AIMimeProfile);
}

public static bool IsAiChatCompletion(this ContentType contentType)
{
return contentType != null &&
contentType.IsJson() &&
!contentType.IsFeatureFlag() &&
!contentType.IsKeyVaultReference() &&
contentType.Parameters.ContainsKey("profile") &&
!string.IsNullOrEmpty(contentType.Parameters["profile"]) &&
contentType.Parameters["profile"].StartsWith(RequestTracingConstants.AIChatCompletionMimeProfile);
}

public static bool IsJson(this ContentType contentType)
{
if (contentType == null)
{
return false;
}

string acceptedMainType = "application";
string acceptedSubType = "json";

ReadOnlySpan<char> mediaTypeSpan = contentType.MediaType.AsSpan();

// Since contentType has been validated using System.Net.Mime.ContentType,
// mediaType will always have exactly 2 parts after splitting on '/'
int slashIndex = mediaTypeSpan.IndexOf('/');

if (mediaTypeSpan.Slice(0, slashIndex).Equals(acceptedMainType.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
ReadOnlySpan<char> subTypeSpan = mediaTypeSpan.Slice(slashIndex + 1);

while (!subTypeSpan.IsEmpty)
{
int plusIndex = subTypeSpan.IndexOf('+');

ReadOnlySpan<char> currentSubType = plusIndex == -1 ? subTypeSpan : subTypeSpan.Slice(0, plusIndex);

if (currentSubType.Equals(acceptedSubType.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return true;
}

subTypeSpan = plusIndex == -1 ? ReadOnlySpan<char>.Empty : subTypeSpan.Slice(plusIndex + 1);
}
}

return false;
}

public static bool IsFeatureFlag(this ContentType contentType)
{
return contentType.MediaType.Equals(FeatureManagementConstants.ContentType);
}

public static bool IsKeyVaultReference(this ContentType contentType)
{
return contentType.MediaType.Equals(KeyVaultConstants.ContentType);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,40 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using System;
using System.Net.Mime;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
{
internal static class StringExtensions
{
public static bool TryParseContentType(this string contentTypeString, out ContentType contentType)
{
contentType = null;

if (string.IsNullOrWhiteSpace(contentTypeString))
{
return false;
}

try
{
contentType = new ContentType(contentTypeString.Trim());

return true;
}
catch (FormatException)
{
return false;
}
catch (IndexOutOfRangeException)
{
// Bug in System.Net.Mime.ContentType throws this if contentType is "xyz/"
// https://github.com/dotnet/runtime/issues/39337
return false;
}
}

public static string NormalizeNull(this string s)
{
return s == LabelFilter.Null ? null : s;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -45,10 +46,20 @@ public Task<IEnumerable<KeyValuePair<string, string>>> ProcessKeyValue(Configura

public bool CanProcess(ConfigurationSetting setting)
{
string contentType = setting?.ContentType?.Split(';')[0].Trim();
if (setting == null ||
string.IsNullOrWhiteSpace(setting.Value) ||
string.IsNullOrWhiteSpace(setting.ContentType))
{
return false;
}

if (setting.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker))
{
return true;
}

return string.Equals(contentType, FeatureManagementConstants.ContentType) ||
setting.Key.StartsWith(FeatureManagementConstants.FeatureFlagMarker);
return setting.ContentType.TryParseContentType(out ContentType contentType) &&
contentType.IsFeatureFlag();
}

public bool NeedsRefresh()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Azure.Data.AppConfiguration;
using Azure;
using Azure;
using Azure.Data.AppConfiguration;
using System.Collections.Generic;

namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
Expand Down
Loading