src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs (291 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Azure.Core;
using Azure.Data.AppConfiguration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.AzureKeyVault;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement;
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration
{
/// <summary>
/// Options used to configure the behavior of an Azure App Configuration provider.
/// If neither <see cref="Select"/> nor <see cref="SelectSnapshot"/> is ever called, all key-values with no label are included in the configuration provider.
/// </summary>
public class AzureAppConfigurationOptions
{
private const int MaxRetries = 2;
private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromMinutes(1);
private static readonly KeyValueSelector DefaultQuery = new KeyValueSelector { KeyFilter = KeyFilter.Any, LabelFilter = LabelFilter.Null };
private List<KeyValueWatcher> _individualKvWatchers = new List<KeyValueWatcher>();
private List<KeyValueWatcher> _ffWatchers = new List<KeyValueWatcher>();
private List<IKeyValueAdapter> _adapters;
private List<Func<ConfigurationSetting, ValueTask<ConfigurationSetting>>> _mappers = new List<Func<ConfigurationSetting, ValueTask<ConfigurationSetting>>>();
private List<KeyValueSelector> _selectors;
private IConfigurationRefresher _refresher = new AzureAppConfigurationRefresher();
private bool _selectCalled = false;
// The following set is sorted in descending order.
// Since multiple prefixes could start with the same characters, we need to trim the longest prefix first.
private SortedSet<string> _keyPrefixes = new SortedSet<string>(Comparer<string>.Create((k1, k2) => -string.Compare(k1, k2, StringComparison.OrdinalIgnoreCase)));
/// <summary>
/// Flag to indicate whether replica discovery is enabled.
/// </summary>
public bool ReplicaDiscoveryEnabled { get; set; } = true;
/// <summary>
/// Flag to indicate whether load balancing is enabled.
/// </summary>
public bool LoadBalancingEnabled { get; set; }
/// <summary>
/// The list of connection strings used to connect to an Azure App Configuration store and its replicas.
/// </summary>
internal IEnumerable<string> ConnectionStrings { get; private set; }
/// <summary>
/// The list of endpoints of an Azure App Configuration store.
/// If this property is set, the <see cref="Credential"/> property also needs to be set.
/// </summary>
internal IEnumerable<Uri> Endpoints { get; private set; }
/// <summary>
/// The credential used to connect to the Azure App Configuration.
/// If this property is set, the <see cref="Endpoints"/> property also needs to be set.
/// </summary>
internal TokenCredential Credential { get; private set; }
/// <summary>
/// A collection of <see cref="KeyValueSelector"/> specified by user.
/// </summary>
internal IEnumerable<KeyValueSelector> Selectors => _selectors;
/// <summary>
/// Indicates if <see cref="AzureAppConfigurationRefreshOptions.RegisterAll"/> was called.
/// </summary>
internal bool RegisterAllEnabled { get; private set; }
/// <summary>
/// Refresh interval for selected key-value collections when <see cref="AzureAppConfigurationRefreshOptions.RegisterAll"/> is called.
/// </summary>
internal TimeSpan KvCollectionRefreshInterval { get; private set; }
/// <summary>
/// A collection of <see cref="KeyValueWatcher"/>.
/// </summary>
internal IEnumerable<KeyValueWatcher> IndividualKvWatchers => _individualKvWatchers;
/// <summary>
/// A collection of <see cref="KeyValueWatcher"/>.
/// </summary>
internal IEnumerable<KeyValueWatcher> FeatureFlagWatchers => _ffWatchers;
/// <summary>
/// A collection of <see cref="IKeyValueAdapter"/>.
/// </summary>
internal IEnumerable<IKeyValueAdapter> Adapters
{
get => _adapters;
set => _adapters = value?.ToList();
}
/// <summary>
/// A collection of user defined functions that transform each <see cref="ConfigurationSetting"/>.
/// </summary>
internal IEnumerable<Func<ConfigurationSetting, ValueTask<ConfigurationSetting>>> Mappers => _mappers;
/// <summary>
/// A collection of key prefixes to be trimmed.
/// </summary>
internal IEnumerable<string> KeyPrefixes => _keyPrefixes;
/// <summary>
/// For use in tests only. An optional configuration client manager that can be used to provide clients to communicate with Azure App Configuration.
/// </summary>
internal IConfigurationClientManager ClientManager { get; set; }
/// <summary>
/// For use in tests only. An optional class used to process pageable results from Azure App Configuration.
/// </summary>
internal IConfigurationSettingPageIterator ConfigurationSettingPageIterator { get; set; }
/// <summary>
/// An optional timespan value to set the minimum backoff duration to a value other than the default.
/// </summary>
internal TimeSpan MinBackoffDuration { get; set; } = FailOverConstants.MinBackoffDuration;
/// <summary>
/// Options used to configure the client used to communicate with Azure App Configuration.
/// </summary>
internal ConfigurationClientOptions ClientOptions { get; } = GetDefaultClientOptions();
/// <summary>
/// Flag to indicate whether Key Vault options have been configured.
/// </summary>
internal bool IsKeyVaultConfigured { get; private set; } = false;
/// <summary>
/// Flag to indicate whether Key Vault secret values will be refreshed automatically.
/// </summary>
internal bool IsKeyVaultRefreshConfigured { get; private set; } = false;
/// <summary>
/// Indicates all feature flag features used by the application.
/// </summary>
internal FeatureFlagTracing FeatureFlagTracing { get; set; } = new FeatureFlagTracing();
/// <summary>
/// Options used to configure provider startup.
/// </summary>
internal StartupOptions Startup { get; set; } = new StartupOptions();
/// <summary>
/// Initializes a new instance of the <see cref="AzureAppConfigurationOptions"/> class.
/// </summary>
public AzureAppConfigurationOptions()
{
_adapters = new List<IKeyValueAdapter>()
{
new AzureKeyVaultKeyValueAdapter(new AzureKeyVaultSecretProvider()),
new JsonKeyValueAdapter(),
new FeatureManagementKeyValueAdapter(FeatureFlagTracing)
};
// Adds the default query to App Configuration if <see cref="Select"/> and <see cref="SelectSnapshot"/> are never called.
_selectors = new List<KeyValueSelector> { DefaultQuery };
}
/// <summary>
/// Specify what key-values to include in the configuration provider.
/// <see cref="Select"/> can be called multiple times to include multiple sets of key-values.
/// </summary>
/// <param name="keyFilter">
/// The key filter to apply when querying Azure App Configuration for key-values.
/// An asterisk (*) can be added to the end to return all key-values whose key begins with the key filter.
/// e.g. key filter `abc*` returns all key-values whose key starts with `abc`.
/// A comma (,) can be used to select multiple key-values. Comma separated filters must exactly match a key to select it.
/// Using asterisk to select key-values that begin with a key filter while simultaneously using comma separated key filters is not supported.
/// E.g. the key filter `abc*,def` is not supported. The key filters `abc*` and `abc,def` are supported.
/// For all other cases the characters: asterisk (*), comma (,), and backslash (\) are reserved. Reserved characters must be escaped using a backslash (\).
/// e.g. the key filter `a\\b\,\*c*` returns all key-values whose key starts with `a\b,*c`.
/// Built-in key filter options: <see cref="KeyFilter"/>.
/// </param>
/// <param name="labelFilter">
/// The label filter to apply when querying Azure App Configuration for key-values. By default the null label will be used. Built-in label filter options: <see cref="LabelFilter"/>
/// The characters asterisk (*) and comma (,) are not supported. Backslash (\) character is reserved and must be escaped using another backslash (\).
/// </param>
/// <param name="tagFilters">
/// In addition to key and label filters, key-values from Azure App Configuration can be filtered based on their tag names and values.
/// Each tag filter must follow the format "tagName=tagValue". Only those key-values will be loaded whose tags match all the tags provided here.
/// Built in tag filter values: <see cref="TagValue"/>. For example, $"tagName={<see cref="TagValue.Null"/>}".
/// The characters asterisk (*), comma (,) and backslash (\) are reserved and must be escaped using a backslash (\).
/// Up to 5 tag filters can be provided. If no tag filters are provided, key-values will not be filtered based on tags.
/// </param>
public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter = LabelFilter.Null, IEnumerable<string> tagFilters = null)
{
if (string.IsNullOrEmpty(keyFilter))
{
throw new ArgumentNullException(nameof(keyFilter));
}
// Do not support * and , for label filter for now.
if (labelFilter != null && (labelFilter.Contains('*') || labelFilter.Contains(',')))
{
throw new ArgumentException("The characters '*' and ',' are not supported in label filters.", nameof(labelFilter));
}
if (string.IsNullOrWhiteSpace(labelFilter))
{
labelFilter = LabelFilter.Null;
}
if (tagFilters != null)
{
foreach (string tag in tagFilters)
{
if (string.IsNullOrEmpty(tag) || !tag.Contains('=') || tag.IndexOf('=') == 0)
{
throw new ArgumentException($"Tag filter '{tag}' does not follow the format \"tagName=tagValue\".", nameof(tagFilters));
}
}
}
if (!_selectCalled)
{
_selectors.Remove(DefaultQuery);
_selectCalled = true;
}
_selectors.AppendUnique(new KeyValueSelector
{
KeyFilter = keyFilter,
LabelFilter = labelFilter,
TagFilters = tagFilters
});
return this;
}
/// <summary>
/// Specify a snapshot and include its contained key-values in the configuration provider.
/// <see cref="SelectSnapshot"/> can be called multiple times to include key-values from multiple snapshots.
/// </summary>
/// <param name="name">The name of the snapshot in Azure App Configuration.</param>
public AzureAppConfigurationOptions SelectSnapshot(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
if (!_selectCalled)
{
_selectors.Remove(DefaultQuery);
_selectCalled = true;
}
_selectors.AppendUnique(new KeyValueSelector
{
SnapshotName = name
});
return this;
}
/// <summary>
/// Configures options for Azure App Configuration feature flags that will be parsed and transformed into feature management configuration.
/// If no filtering is specified via the <see cref="FeatureFlagOptions"/> then all feature flags with no label are loaded.
/// All loaded feature flags will be automatically registered for refresh as a collection.
/// </summary>
/// <param name="configure">A callback used to configure feature flag options.</param>
public AzureAppConfigurationOptions UseFeatureFlags(Action<FeatureFlagOptions> configure = null)
{
FeatureFlagOptions options = new FeatureFlagOptions();
configure?.Invoke(options);
if (options.RefreshInterval < RefreshConstants.MinimumFeatureFlagRefreshInterval)
{
throw new ArgumentOutOfRangeException(nameof(options.RefreshInterval), options.RefreshInterval.TotalMilliseconds,
string.Format(ErrorMessages.RefreshIntervalTooShort, RefreshConstants.MinimumFeatureFlagRefreshInterval.TotalMilliseconds));
}
if (options.FeatureFlagSelectors.Count() != 0 && options.Label != null)
{
throw new InvalidOperationException($"Please select feature flags by either the {nameof(options.Select)} method or by setting the {nameof(options.Label)} property, not both.");
}
if (options.FeatureFlagSelectors.Count() == 0)
{
// Select clause is not present
options.FeatureFlagSelectors.Add(new KeyValueSelector
{
KeyFilter = FeatureManagementConstants.FeatureFlagMarker + "*",
LabelFilter = string.IsNullOrWhiteSpace(options.Label) ? LabelFilter.Null : options.Label,
IsFeatureFlagSelector = true
});
}
foreach (KeyValueSelector featureFlagSelector in options.FeatureFlagSelectors)
{
_selectors.AppendUnique(featureFlagSelector);
_ffWatchers.AppendUnique(new KeyValueWatcher
{
Key = featureFlagSelector.KeyFilter,
Label = featureFlagSelector.LabelFilter,
Tags = featureFlagSelector.TagFilters,
// If UseFeatureFlags is called multiple times for the same key and label filters, last refresh interval wins
RefreshInterval = options.RefreshInterval
});
}
return this;
}
/// <summary>
/// Connect the provider to the Azure App Configuration service via a connection string.
/// </summary>
/// <param name="connectionString">
/// Used to authenticate with Azure App Configuration.
/// </param>
public AzureAppConfigurationOptions Connect(string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ArgumentNullException(nameof(connectionString));
}
return Connect(new List<string> { connectionString });
}
/// <summary>
/// Connect the provider to an Azure App Configuration store and its replicas via a list of connection strings.
/// </summary>
/// <param name="connectionStrings">
/// Used to authenticate with Azure App Configuration.
/// </param>
public AzureAppConfigurationOptions Connect(IEnumerable<string> connectionStrings)
{
if (connectionStrings == null || !connectionStrings.Any())
{
throw new ArgumentNullException(nameof(connectionStrings));
}
if (connectionStrings.Distinct().Count() != connectionStrings.Count())
{
throw new ArgumentException($"All values in '{nameof(connectionStrings)}' must be unique.");
}
Endpoints = null;
Credential = null;
ConnectionStrings = connectionStrings;
return this;
}
/// <summary>
/// Connect the provider to Azure App Configuration using endpoint and token credentials.
/// </summary>
/// <param name="endpoint">The endpoint of the Azure App Configuration to connect to.</param>
/// <param name="credential">Token credentials to use to connect.</param>
public AzureAppConfigurationOptions Connect(Uri endpoint, TokenCredential credential)
{
if (endpoint == null)
{
throw new ArgumentNullException(nameof(endpoint));
}
if (credential == null)
{
throw new ArgumentNullException(nameof(credential));
}
return Connect(new List<Uri>() { endpoint }, credential);
}
/// <summary>
/// Connect the provider to an Azure App Configuration store and its replicas using a list of endpoints and a token credential.
/// </summary>
/// <param name="endpoints">The list of endpoints of an Azure App Configuration store and its replicas to connect to.</param>
/// <param name="credential">Token credential to use to connect.</param>
public AzureAppConfigurationOptions Connect(IEnumerable<Uri> endpoints, TokenCredential credential)
{
if (endpoints == null || !endpoints.Any())
{
throw new ArgumentNullException(nameof(endpoints));
}
if (endpoints.Distinct(new EndpointComparer()).Count() != endpoints.Count())
{
throw new ArgumentException($"All values in '{nameof(endpoints)}' must be unique.");
}
Credential = credential ?? throw new ArgumentNullException(nameof(credential));
Endpoints = endpoints;
ConnectionStrings = null;
return this;
}
/// <summary>
/// Trims the provided prefix from the keys of all key-values retrieved from Azure App Configuration.
/// </summary>
/// <param name="prefix">The prefix to be trimmed.</param>
public AzureAppConfigurationOptions TrimKeyPrefix(string prefix)
{
if (string.IsNullOrEmpty(prefix))
{
throw new ArgumentNullException(nameof(prefix));
}
_keyPrefixes.Add(prefix);
return this;
}
/// <summary>
/// Configure the client(s) used to communicate with Azure App Configuration.
/// </summary>
/// <param name="configure">A callback used to configure Azure App Configuration client options.</param>
public AzureAppConfigurationOptions ConfigureClientOptions(Action<ConfigurationClientOptions> configure)
{
configure?.Invoke(ClientOptions);
return this;
}
/// <summary>
/// Configure refresh for key-values in the configuration provider.
/// </summary>
/// <param name="configure">A callback used to configure Azure App Configuration refresh options.</param>
public AzureAppConfigurationOptions ConfigureRefresh(Action<AzureAppConfigurationRefreshOptions> configure)
{
if (RegisterAllEnabled)
{
throw new InvalidOperationException($"{nameof(ConfigureRefresh)}() cannot be invoked multiple times when {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)} has been invoked.");
}
var refreshOptions = new AzureAppConfigurationRefreshOptions();
configure?.Invoke(refreshOptions);
bool isRegisterCalled = refreshOptions.RefreshRegistrations.Any();
RegisterAllEnabled = refreshOptions.RegisterAllEnabled;
if (!isRegisterCalled && !RegisterAllEnabled)
{
throw new InvalidOperationException($"{nameof(ConfigureRefresh)}() must call either {nameof(AzureAppConfigurationRefreshOptions.Register)}()" +
$" or {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)}()");
}
// Check if both register methods are called at any point
if (RegisterAllEnabled && (_individualKvWatchers.Any() || isRegisterCalled))
{
throw new InvalidOperationException($"Cannot call both {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)} and "
+ $"{nameof(AzureAppConfigurationRefreshOptions.Register)}.");
}
if (RegisterAllEnabled)
{
KvCollectionRefreshInterval = refreshOptions.RefreshInterval;
}
else
{
foreach (KeyValueWatcher item in refreshOptions.RefreshRegistrations)
{
item.RefreshInterval = refreshOptions.RefreshInterval;
_individualKvWatchers.Add(item);
}
}
return this;
}
/// <summary>
/// Get an instance of <see cref="IConfigurationRefresher"/> that can be used to trigger a refresh for the registered key-values.
/// </summary>
/// <returns>An instance of <see cref="IConfigurationRefresher"/>.</returns>
public IConfigurationRefresher GetRefresher()
{
return _refresher;
}
/// <summary>
/// Configures the Azure App Configuration provider to use the provided Key Vault configuration to resolve key vault references.
/// </summary>
/// <param name="configure">A callback used to configure Azure App Configuration key vault options.</param>
public AzureAppConfigurationOptions ConfigureKeyVault(Action<AzureAppConfigurationKeyVaultOptions> configure)
{
var keyVaultOptions = new AzureAppConfigurationKeyVaultOptions();
configure?.Invoke(keyVaultOptions);
if (keyVaultOptions.Credential != null && keyVaultOptions.SecretResolver != null)
{
throw new InvalidOperationException($"Cannot configure both default credentials and secret resolver for Key Vault references. Please call either {nameof(keyVaultOptions.SetCredential)} or {nameof(keyVaultOptions.SetSecretResolver)} method, not both.");
}
_adapters.RemoveAll(a => a is AzureKeyVaultKeyValueAdapter);
_adapters.Add(new AzureKeyVaultKeyValueAdapter(new AzureKeyVaultSecretProvider(keyVaultOptions)));
IsKeyVaultRefreshConfigured = keyVaultOptions.IsKeyVaultRefreshConfigured;
IsKeyVaultConfigured = true;
return this;
}
/// <summary>
/// Provides a way to transform settings retrieved from App Configuration before they are processed by the configuration provider.
/// </summary>
/// <param name="mapper">A callback registered by the user to transform each configuration setting.</param>
public AzureAppConfigurationOptions Map(Func<ConfigurationSetting, ValueTask<ConfigurationSetting>> mapper)
{
if (mapper == null)
{
throw new ArgumentNullException(nameof(mapper));
}
_mappers.Add(mapper);
return this;
}
/// <summary>
/// Configure the provider behavior when loading data from Azure App Configuration on startup.
/// </summary>
/// <param name="configure">A callback used to configure Azure App Configuration startup options.</param>
public AzureAppConfigurationOptions ConfigureStartupOptions(Action<StartupOptions> configure)
{
configure?.Invoke(Startup);
return this;
}
private static ConfigurationClientOptions GetDefaultClientOptions()
{
var clientOptions = new ConfigurationClientOptions(ConfigurationClientOptions.ServiceVersion.V2023_10_01);
clientOptions.Retry.MaxRetries = MaxRetries;
clientOptions.Retry.MaxDelay = MaxRetryDelay;
clientOptions.Retry.Mode = RetryMode.Exponential;
clientOptions.AddPolicy(new UserAgentHeaderPolicy(), HttpPipelinePosition.PerCall);
return clientOptions;
}
}
}