Skip to content

Conversation

@jimmyca15
Copy link
Member

@jimmyca15 jimmyca15 commented Dec 14, 2021

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.

  • Update types to respect dynamic terminology. E.g. IFeatureManager -> IDynamicFeatureManager.
    • Updated property/method summaries to distinguish dynamic features and feature flags.
  • 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 IDynamicFeatureManagerSnapshot
  • Added test for custom feature definition provider.
  • Added test for backcompat support for v1 configuration schema.

Old configuration schema example

{
    "FeatureManagement": {
        "FeatureFlag1": true,
        "FeatureFlag2": {
            "EnabledFor": [
                ...
            ]
        }
    }
}

New configuration schema example

{
    "FeatureManagement": {
        "FeatureFlags": {
            "FeatureFlag1": true,
            "FeatureFlag2": {
                "EnabledFor": [
                    ...
                ]
            }
        },
        "DynamicFeatures": {
            "DynamicFeature1": {
                "Assigner": "Targeting",
                "ConfigurationReference": "SomeSettings",
                "AssignmentParameters": {
                    ...
                }
            }
        }
    }
}

* 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);
Copy link
Member

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);
Copy link
Member

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>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

variantManager

NIT: update variable name variantManager

@@ -1,5 +1,5 @@
@using Microsoft.FeatureManagement
@inject IFeatureVariantManager variantManager;
@inject IDynamicFeatureManager variantManager;
Copy link
Member

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>();
Copy link
Member

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))
Copy link
Member

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
Copy link
Member

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?

Copy link
Member Author

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?

Copy link
Member

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).

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancellationToken

Should we default the cancellationToken?

Looks like we don't have cancellationToken in I*FeatureFilter, should we add it?

Copy link
Member Author

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.

Copy link
Member Author

@jimmyca15 jimmyca15 Jan 7, 2022

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

featureName

NIT: featureName -> featureFlagName

/// <summary>
/// A provider of feature definitions.
/// </summary>
public interface IFeatureDefinitionProvider
Copy link
Member

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?

Copy link
Member Author

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);
Copy link
Member

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cancellationToken = default

default not needed here. Cancellation token already has a default value in the interface.

*/

var enabledFor = new List<FeatureFilterConfiguration>();
Debug.Assert(configurationSection != null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug.Assert(configurationSection != null);

Should we throw an exception instead of assert?

Copy link
Member Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug.Assert(configurationSection != null);

Same here. Null check may not even be necessary since the calling methods already check for null before invoking these two methods.

Copy link
Member Author

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.

  1. 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 configurationSection is not null.
  2. 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.
  3. 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.
@jimmyca15 jimmyca15 force-pushed the user/jimmyca/schemaChange branch from 08d38af to f9b661b Compare January 7, 2022 23:42
return assigner;
}

private static bool IsMatchingMetadataName(string metadataName, string desiredName, string suffix)
Copy link
Member

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?

Copy link
Member Author

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.

@zhenlan
Copy link
Member

zhenlan commented Jan 10, 2022

using System;

Missing copyright header


Refers to: src/Microsoft.FeatureManagement/NameHelper.cs:1 in ba664ee. [](commit_id = ba664ee, deletion_comment = False)

@jimmyca15 jimmyca15 merged commit fd51d97 into microsoft:feature/v3 Jan 10, 2022
jimmyca15 added a commit that referenced this pull request Jun 10, 2022
* 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants