Skip to content
Merged
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 Expand Up @@ -1202,6 +1221,13 @@ await ExecuteWithFailOverPolicyAsync<object>(clients, async (client) =>

private bool IsFailOverable(AggregateException ex)
{
TaskCanceledException tce = ex.InnerExceptions?.LastOrDefault(e => e is TaskCanceledException) as TaskCanceledException;

if (tce != null && tce.InnerException is TimeoutException)
{
return true;
}

RequestFailedException rfe = ex.InnerExceptions?.LastOrDefault(e => e is RequestFailedException) as RequestFailedException;

return rfe != null ? IsFailOverable(rfe) : false;
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