-
Notifications
You must be signed in to change notification settings - Fork 119
Update to dynamic features. #157
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
Update to dynamic features. #157
Conversation
* Update types to respect dynamic feature name. E.g. IFeatureManager -> IDynamicFeatureManager. * Updated property/method summaries to distinguish dynamic features and feature flags. * Added IDynamicFeatureManagerSnapshot * Separate feature flags and dynamic features in configuration schema * Backward compatability support for old configuration schema. * Breaking: Update custom feature definition provider to return feature flags and dynamic features. * Added DynamicFeatureDefinition class * renamed FeatureDefinition to FeatureFlagDefinition. * Breaking: IFeatureManager.GetFeatureNamesAsync renamed to IFeatureManager.GetFeatureFlagNamesAsync * Corrected namespace for built-in feature variant assigners. * Microsoft.FeatureManagement -> Microsoft.FeatureManagement.Assigners * Added test for custom feature definition provider. * Added test for backcompat support for v1 configuration schema.
| /// <param name="feature">The name of the dynamic feature.</param> | ||
| /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||
| /// <returns>A typed representation of the feature variant that should be used for a given dynamic feature.</returns> | ||
| ValueTask<T> GetVariantAsync<T>(string feature, CancellationToken cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add default for cancellationToken
| /// <param name="context">A context providing information that can be used to evaluate which variant should be used for the dynamic feature.</param> | ||
| /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||
| /// <returns>A typed representation of the feature variant's configuration that should be used for a given feature.</returns> | ||
| ValueTask<T> GetVariantAsync<T, TContext>(string feature, TContext context, CancellationToken cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add default for cancellationToken
| using (ServiceProvider serviceProvider = services.BuildServiceProvider()) | ||
| { | ||
| IFeatureVariantManager variantManager = serviceProvider.GetRequiredService<IFeatureVariantManager>(); | ||
| IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService<IDynamicFeatureManager>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @@ -1,5 +1,5 @@ | |||
| @using Microsoft.FeatureManagement | |||
| @inject IFeatureVariantManager variantManager; | |||
| @inject IDynamicFeatureManager variantManager; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: update variable name variantManager too?
| { | ||
| IFeatureManager featureManager = serviceProvider.GetRequiredService<IFeatureManager>(); | ||
| IFeatureVariantManager variantManager = serviceProvider.GetRequiredService<IFeatureVariantManager>(); | ||
| IDynamicFeatureManager variantManager = serviceProvider.GetRequiredService<IDynamicFeatureManager>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: update variable name variantManager to dynamicFeatureManager?
| { | ||
| var dynamicFeatureNames = new List<string>(); | ||
|
|
||
| await foreach (string featureName in _featureManager.GetFeatureFlagNamesAsync(cancellationToken).ConfigureAwait(false)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be
_dynamicFeatureManager.GetDynamicFeatureNamesAsync
| /// Used to evaluate whether a feature is enabled or disabled. | ||
| /// </summary> | ||
| class FeatureManager : IFeatureManager, IFeatureVariantManager | ||
| class FeatureManager : IFeatureManager, IDynamicFeatureManager |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking whether we should split the implementation of two interfaces into separate classes. They are pretty much independent. This could be confusing for someone new to look at this code in the future. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the value immediately. What is the confusion that you had in mind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess the question can be asked in the other way - why do we implement both interfaces in one class? Would we do the same if we started from scratch? The functionalities of the two interfaces are parallel. It's not like one is a complement to another. In current naming, feels like the FeatureManager is really for IFeatureManager, and the IDynamicFeatureManager is a supplement (like IDisposable).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we do the same if we started from scratch?
I think that's a great way to look at it. I'd say we'd have them separated. Let me separate them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In current naming, feels like the FeatureManager is really for IFeatureManager, and the IDynamicFeatureManager
For this point, it would be different if we renamed IFeatureManager to IFeatureFlagManager. But that was a breaking change that we didn't consider.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this point, it would be different if we renamed IFeatureManager to IFeatureFlagManager. But that was a breaking change that we didn't consider.
Totally, that would make more sense. Another option is to add what's in IDynamicFeatureManager to IFeatureManager directly. I think that's what we would do if we started from scratch, but we worried it will break all existing libraries built on top of the current feature management library.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, to be honest I'm not completely sold on either side. If you think it'll be more clear I'm happy to separate them. Otherwise, can keep in in FeatureManager
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should separate the implementation if we decide to separate the interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case I'll go ahead and separate ConfigurationFeatureDefinitionProvider and FeatureManagerSnapshot as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
| /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||
| /// <returns>The variant that should be assigned for a given feature.</returns> | ||
| /// <returns>The variant that should be assigned for a given dynamic feature.</returns> | ||
| ValueTask<FeatureVariant> AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, TContext appContext, CancellationToken cancellationToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we don't have cancellationToken in I*FeatureFilter, should we add it?
cancellation tokens on existing interfaces were added in PRs checked in to the main branch.
For that reason I didn't touch them here. This PR is going into "feature/v3" branch and when "feature/v3" merges into main those conflicts will be resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Circling back to this, there were some missing default cancellation tokens on the new dynamic feature interfaces. Thanks. I'm adding them now.
| /// <returns>The feature's definition.</returns> | ||
| Task<FeatureDefinition> GetFeatureDefinitionAsync(string featureName, CancellationToken cancellationToken = default); | ||
| /// <returns>The feature flag's definition.</returns> | ||
| Task<FeatureFlagDefinition> GetFeatureFlagDefinitionAsync(string featureName, CancellationToken cancellationToken = default); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// <summary> | ||
| /// A provider of feature definitions. | ||
| /// </summary> | ||
| public interface IFeatureDefinitionProvider |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we split it into separate interfaces for feature flags and dynamic features so users don't have to implement both if they only care about one but not the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We split IFeatureManager and IDynamicFeatureManager so I'm not opposed to carrying that over here.
| /// <param name="cancellationToken">The cancellation token to cancel the operation.</param> | ||
| /// <returns>True if the feature is enabled, otherwise false.</returns> | ||
| /// <returns>True if the feature flag is enabled, otherwise false.</returns> | ||
| Task<bool> IsEnabledAsync(string feature, CancellationToken cancellationToken = default); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feature -> featureFlag?
| } | ||
| } | ||
|
|
||
| public async IAsyncEnumerable<string> GetDynamicFeatureNamesAsync([EnumeratorCancellation]CancellationToken cancellationToken = default) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| */ | ||
|
|
||
| var enabledFor = new List<FeatureFilterConfiguration>(); | ||
| Debug.Assert(configurationSection != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a private method so throwing here wouldn't be expected.
|
|
||
| private DynamicFeatureDefinition ReadDynamicFeatureDefinition(IConfigurationSection configurationSection) | ||
| { | ||
| Debug.Assert(configurationSection != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We often Debug.Assert use Debug.Assert in our private methods. It offers a couple benefits.
- It makes it clear that the assertion can be relied on in the rest of the method execution. In this case, the reader will know the
configurationSectionis not null. - When we look at this method we know that if we're calling it from another method we shouldn't pass null, and vice versa we can look at the places that call this and confirm that they're doing a null check, like you mentioned, before calling.
- If it is ever null for some reason, it will throw in debug mode so it can be fixed.
…ntation in FeatureManager, ConfigurationFeatureDefinitionProvider, and FeatureManagerSnapshot.. * Separated IConfigurationDefinitionProvider into IFeatureFlagDefinitionProvider and IDynamicFeatureDefinitionProvider * Added missing default cancellation tokens.
08d38af to
f9b661b
Compare
| return assigner; | ||
| } | ||
|
|
||
| private static bool IsMatchingMetadataName(string metadataName, string desiredName, string suffix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comments in this function are still talking about filters. Is there a place we can put this helper function to so it can be shared?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. I put it in a new helper, NameHelper.
* Add support for feature variants. * Renaming and assorted fixes. * Remove unnecessary field. * * Added additional argument validation. * Added additional dynamic feature configuration validation. * Fixed bug in audience accumulation for variant targeting * Added test for variant targeting audience accumulation. * Since filter aliases are case insensitive, update filter cache to reflect this. * * Fixed issue with filter + assigner suffix references. * Clarified error types. * Updated/added tests * Share suffix matching logic. Corrected variable name. * Removed unnecessary helper methods. Updated error codes for consistency. * Clarify filter selection logic in feature evaluation and add checks for impossible contextual evaluations. * Updated argument validation messages. Updated exception used for null child property. * Update to dynamic features. (#157) * Update to dynamic features. * Update types to respect dynamic feature name. E.g. IFeatureManager -> IDynamicFeatureManager. * Updated property/method summaries to distinguish dynamic features and feature flags. * Added IDynamicFeatureManagerSnapshot * Separate feature flags and dynamic features in configuration schema * Backward compatability support for old configuration schema. * Breaking: Update custom feature definition provider to return feature flags and dynamic features. * Added DynamicFeatureDefinition class * renamed FeatureDefinition to FeatureFlagDefinition. * Breaking: IFeatureManager.GetFeatureNamesAsync renamed to IFeatureManager.GetFeatureFlagNamesAsync * Corrected namespace for built-in feature variant assigners. * Microsoft.FeatureManagement -> Microsoft.FeatureManagement.Assigners * Added test for custom feature definition provider. * Added test for backcompat support for v1 configuration schema. * * Separated feature flag implementation from dynamic feature impelementation in FeatureManager, ConfigurationFeatureDefinitionProvider, and FeatureManagerSnapshot.. * Separated IConfigurationDefinitionProvider into IFeatureFlagDefinitionProvider and IDynamicFeatureDefinitionProvider * Added missing default cancellation tokens. * Use shared helper for filter/assigner reference matching. * Add copyright header. * Added dynamic feature documentation. (#159) * Added dynamic feature documentation. * Readme fixes. * * Added index. * Moved around sections * Removed enum usage. * Fixed mistyped interfaces. * Clarified variant resolution. * Fix links with '.' character. * Updates. * Updated index. * Default -> Name * Remove `MyFeatureFlags` references. * Updated missed "feature name" occurrences. * feature definition -> feature flag * SuggestedReadmeChanges * Updated comments from readme * Fixed targeting assignment precedence. * Remove unused variable. Update method signature. Added missing declaration in test. * Add null/empty check for feature evaluation. * Added .NET 6 as a target framework and removed .NET 5 * Update Azure App Configuration provider reference in example project to pull features using v2 schema. * Build with .NET 6 (#186) * Build with .NET 6 * Update build step name. * Add link to v2 readme. (#184) * Throw exception if feature management schemas are mixed to avoid unintentionally hiding feature flags. (#187) Co-authored-by: mrm9084 <[email protected]>
This PR contains multiple changes. The two biggest changes are the separation of feature flags and dynamic features and the renaming of types to respect dynamic feature terminology. Most other changes follow as consequence. Details of the included changes are below.
Documentation updates, including the new configuration schema, to follow in a subsequent PR.
Old configuration schema example
New configuration schema example