Skip to content

Commit c740e7c

Browse files
committed
Add RegisterAll API to enable monitoring collections of key-values for refresh (#574)
* WIP * WIP testing out client extensions methods * WIP added selectors to multikeywatchers * remove unused property * WIP check for registerall changes to change refreshall * WIP * WIP fixing types and reslving errors * WIP fixing client extensions class * WIP * WIP update feature flag logic * WIP client extensions * WIP reload all flags on change * WIP * WIP fixing tests to return response for getconfigurationsettingsasync * WIP etag for tests * fix watchedcollections null * WIP tests, working for examples * remove unused variables * update to newest sdk version, remove unused * WIP fixing tests * WIP reworking testing to work with new etag approach * tests passing, fix mockasyncpageable * update sdk package version * fix loghelper, tests * WIP fixing aspages tests * revert watchesfeatureflags test * update test again * WIP * fixing watchconditions * separate selected key value collections from feature flag collections, separate selectors, add new methods to support new logic * comment and naming updates * fixing unit tests, namespace of defining/calling code needs to be same * fixing tests using AsPages * fix tests with pageablemanager * format * fix tests * fix tests * remove unused extension test class * fix comment, capitalization * check etag on 200, fix tests * add registerall test, fix refresh tests * fix condition for pages and old match conditions * WIP fixing PR comments, tests * check status after advancing existing etag enumerator * move around refresh logic * null check page etag, revert break to existing keys check in getrefreshedcollections * fix loadselected, replace selectedkvwatchers with registerall refresh time * fix comment in options * clean up tests * PR comments * PR comments * don't allow both registerall and register * fix check for calls to both register methods * PR comments for rename/small changes * fix compile error * simplify refreshasync path, fix naming from comments * remove redundant if check * simplify logic for minrefreshinterval * fix smaller comments * call loadselected when refreshing collection, separate data for individual refresh * in progress change to registerall include ff * fix load order * fix comments, rename logging constants to match new behavior * pr comments, refactor refreshasync * clean up etags dictionary creation * PR comments * add uncommitted changes to testhelper * update tests for registerall with feature flags, check ff keys to remove flags on refresh * PR comments * PR comments * use invalidoperationexception in configurerefresh, update loggingconstants to match behavior * remove unused changes
1 parent 47165a6 commit c740e7c

17 files changed

+865
-470
lines changed

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/AzureAppConfigurationOptions.cs

Lines changed: 83 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ public class AzureAppConfigurationOptions
2323
private const int MaxRetries = 2;
2424
private static readonly TimeSpan MaxRetryDelay = TimeSpan.FromMinutes(1);
2525

26-
private List<KeyValueWatcher> _changeWatchers = new List<KeyValueWatcher>();
27-
private List<KeyValueWatcher> _multiKeyWatchers = new List<KeyValueWatcher>();
26+
private List<KeyValueWatcher> _individualKvWatchers = new List<KeyValueWatcher>();
27+
private List<KeyValueWatcher> _ffWatchers = new List<KeyValueWatcher>();
2828
private List<IKeyValueAdapter> _adapters;
2929
private List<Func<ConfigurationSetting, ValueTask<ConfigurationSetting>>> _mappers = new List<Func<ConfigurationSetting, ValueTask<ConfigurationSetting>>>();
30-
private List<KeyValueSelector> _kvSelectors = new List<KeyValueSelector>();
30+
private List<KeyValueSelector> _selectors;
3131
private IConfigurationRefresher _refresher = new AzureAppConfigurationRefresher();
32+
private bool _selectCalled = false;
3233

3334
// The following set is sorted in descending order.
3435
// Since multiple prefixes could start with the same characters, we need to trim the longest prefix first.
@@ -62,19 +63,29 @@ public class AzureAppConfigurationOptions
6263
internal TokenCredential Credential { get; private set; }
6364

6465
/// <summary>
65-
/// A collection of <see cref="KeyValueSelector"/>.
66+
/// A collection of <see cref="KeyValueSelector"/> specified by user.
6667
/// </summary>
67-
internal IEnumerable<KeyValueSelector> KeyValueSelectors => _kvSelectors;
68+
internal IEnumerable<KeyValueSelector> Selectors => _selectors;
69+
70+
/// <summary>
71+
/// Indicates if <see cref="AzureAppConfigurationRefreshOptions.RegisterAll"/> was called.
72+
/// </summary>
73+
internal bool RegisterAllEnabled { get; private set; }
74+
75+
/// <summary>
76+
/// Refresh interval for selected key-value collections when <see cref="AzureAppConfigurationRefreshOptions.RegisterAll"/> is called.
77+
/// </summary>
78+
internal TimeSpan KvCollectionRefreshInterval { get; private set; }
6879

6980
/// <summary>
7081
/// A collection of <see cref="KeyValueWatcher"/>.
7182
/// </summary>
72-
internal IEnumerable<KeyValueWatcher> ChangeWatchers => _changeWatchers;
83+
internal IEnumerable<KeyValueWatcher> IndividualKvWatchers => _individualKvWatchers;
7384

7485
/// <summary>
7586
/// A collection of <see cref="KeyValueWatcher"/>.
7687
/// </summary>
77-
internal IEnumerable<KeyValueWatcher> MultiKeyWatchers => _multiKeyWatchers;
88+
internal IEnumerable<KeyValueWatcher> FeatureFlagWatchers => _ffWatchers;
7889

7990
/// <summary>
8091
/// A collection of <see cref="IKeyValueAdapter"/>.
@@ -96,11 +107,15 @@ internal IEnumerable<IKeyValueAdapter> Adapters
96107
internal IEnumerable<string> KeyPrefixes => _keyPrefixes;
97108

98109
/// <summary>
99-
/// An optional configuration client manager that can be used to provide clients to communicate with Azure App Configuration.
110+
/// For use in tests only. An optional configuration client manager that can be used to provide clients to communicate with Azure App Configuration.
100111
/// </summary>
101-
/// <remarks>This property is used only for unit testing.</remarks>
102112
internal IConfigurationClientManager ClientManager { get; set; }
103113

114+
/// <summary>
115+
/// For use in tests only. An optional class used to process pageable results from Azure App Configuration.
116+
/// </summary>
117+
internal IConfigurationSettingPageIterator ConfigurationSettingPageIterator { get; set; }
118+
104119
/// <summary>
105120
/// An optional timespan value to set the minimum backoff duration to a value other than the default.
106121
/// </summary>
@@ -142,6 +157,9 @@ public AzureAppConfigurationOptions()
142157
new JsonKeyValueAdapter(),
143158
new FeatureManagementKeyValueAdapter(FeatureFlagTracing)
144159
};
160+
161+
// Adds the default query to App Configuration if <see cref="Select"/> and <see cref="SelectSnapshot"/> are never called.
162+
_selectors = new List<KeyValueSelector> { new KeyValueSelector { KeyFilter = KeyFilter.Any, LabelFilter = LabelFilter.Null } };
145163
}
146164

147165
/// <summary>
@@ -170,22 +188,30 @@ public AzureAppConfigurationOptions Select(string keyFilter, string labelFilter
170188
throw new ArgumentNullException(nameof(keyFilter));
171189
}
172190

191+
// Do not support * and , for label filter for now.
192+
if (labelFilter != null && (labelFilter.Contains('*') || labelFilter.Contains(',')))
193+
{
194+
throw new ArgumentException("The characters '*' and ',' are not supported in label filters.", nameof(labelFilter));
195+
}
196+
173197
if (string.IsNullOrWhiteSpace(labelFilter))
174198
{
175199
labelFilter = LabelFilter.Null;
176200
}
177201

178-
// Do not support * and , for label filter for now.
179-
if (labelFilter.Contains('*') || labelFilter.Contains(','))
202+
if (!_selectCalled)
180203
{
181-
throw new ArgumentException("The characters '*' and ',' are not supported in label filters.", nameof(labelFilter));
204+
_selectors.Clear();
205+
206+
_selectCalled = true;
182207
}
183208

184-
_kvSelectors.AppendUnique(new KeyValueSelector
209+
_selectors.AppendUnique(new KeyValueSelector
185210
{
186211
KeyFilter = keyFilter,
187212
LabelFilter = labelFilter
188213
});
214+
189215
return this;
190216
}
191217

@@ -201,7 +227,14 @@ public AzureAppConfigurationOptions SelectSnapshot(string name)
201227
throw new ArgumentNullException(nameof(name));
202228
}
203229

204-
_kvSelectors.AppendUnique(new KeyValueSelector
230+
if (!_selectCalled)
231+
{
232+
_selectors.Clear();
233+
234+
_selectCalled = true;
235+
}
236+
237+
_selectors.AppendUnique(new KeyValueSelector
205238
{
206239
SnapshotName = name
207240
});
@@ -212,7 +245,7 @@ public AzureAppConfigurationOptions SelectSnapshot(string name)
212245
/// <summary>
213246
/// Configures options for Azure App Configuration feature flags that will be parsed and transformed into feature management configuration.
214247
/// If no filtering is specified via the <see cref="FeatureFlagOptions"/> then all feature flags with no label are loaded.
215-
/// All loaded feature flags will be automatically registered for refresh on an individual flag level.
248+
/// All loaded feature flags will be automatically registered for refresh as a collection.
216249
/// </summary>
217250
/// <param name="configure">A callback used to configure feature flag options.</param>
218251
public AzureAppConfigurationOptions UseFeatureFlags(Action<FeatureFlagOptions> configure = null)
@@ -237,25 +270,22 @@ public AzureAppConfigurationOptions UseFeatureFlags(Action<FeatureFlagOptions> c
237270
options.FeatureFlagSelectors.Add(new KeyValueSelector
238271
{
239272
KeyFilter = FeatureManagementConstants.FeatureFlagMarker + "*",
240-
LabelFilter = options.Label == null ? LabelFilter.Null : options.Label
273+
LabelFilter = string.IsNullOrWhiteSpace(options.Label) ? LabelFilter.Null : options.Label,
274+
IsFeatureFlagSelector = true
241275
});
242276
}
243277

244-
foreach (var featureFlagSelector in options.FeatureFlagSelectors)
278+
foreach (KeyValueSelector featureFlagSelector in options.FeatureFlagSelectors)
245279
{
246-
var featureFlagFilter = featureFlagSelector.KeyFilter;
247-
var labelFilter = featureFlagSelector.LabelFilter;
280+
_selectors.AppendUnique(featureFlagSelector);
248281

249-
Select(featureFlagFilter, labelFilter);
250-
251-
_multiKeyWatchers.AppendUnique(new KeyValueWatcher
282+
_ffWatchers.AppendUnique(new KeyValueWatcher
252283
{
253-
Key = featureFlagFilter,
254-
Label = labelFilter,
284+
Key = featureFlagSelector.KeyFilter,
285+
Label = featureFlagSelector.LabelFilter,
255286
// If UseFeatureFlags is called multiple times for the same key and label filters, last refresh interval wins
256287
RefreshInterval = options.RefreshInterval
257288
});
258-
259289
}
260290

261291
return this;
@@ -376,18 +406,41 @@ public AzureAppConfigurationOptions ConfigureClientOptions(Action<ConfigurationC
376406
/// <param name="configure">A callback used to configure Azure App Configuration refresh options.</param>
377407
public AzureAppConfigurationOptions ConfigureRefresh(Action<AzureAppConfigurationRefreshOptions> configure)
378408
{
409+
if (RegisterAllEnabled)
410+
{
411+
throw new InvalidOperationException($"{nameof(ConfigureRefresh)}() cannot be invoked multiple times when {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)} has been invoked.");
412+
}
413+
379414
var refreshOptions = new AzureAppConfigurationRefreshOptions();
380415
configure?.Invoke(refreshOptions);
381416

382-
if (!refreshOptions.RefreshRegistrations.Any())
417+
bool isRegisterCalled = refreshOptions.RefreshRegistrations.Any();
418+
RegisterAllEnabled = refreshOptions.RegisterAllEnabled;
419+
420+
if (!isRegisterCalled && !RegisterAllEnabled)
421+
{
422+
throw new InvalidOperationException($"{nameof(ConfigureRefresh)}() must call either {nameof(AzureAppConfigurationRefreshOptions.Register)}()" +
423+
$" or {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)}()");
424+
}
425+
426+
// Check if both register methods are called at any point
427+
if (RegisterAllEnabled && (_individualKvWatchers.Any() || isRegisterCalled))
383428
{
384-
throw new ArgumentException($"{nameof(ConfigureRefresh)}() must have at least one key-value registered for refresh.");
429+
throw new InvalidOperationException($"Cannot call both {nameof(AzureAppConfigurationRefreshOptions.RegisterAll)} and "
430+
+ $"{nameof(AzureAppConfigurationRefreshOptions.Register)}.");
385431
}
386432

387-
foreach (var item in refreshOptions.RefreshRegistrations)
433+
if (RegisterAllEnabled)
434+
{
435+
KvCollectionRefreshInterval = refreshOptions.RefreshInterval;
436+
}
437+
else
388438
{
389-
item.RefreshInterval = refreshOptions.RefreshInterval;
390-
_changeWatchers.Add(item);
439+
foreach (KeyValueWatcher item in refreshOptions.RefreshRegistrations)
440+
{
441+
item.RefreshInterval = refreshOptions.RefreshInterval;
442+
_individualKvWatchers.Add(item);
443+
}
391444
}
392445

393446
return this;

0 commit comments

Comments
 (0)