diff --git a/README.md b/README.md index 29658c5d..1e64e3bb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ASP.NET Core Feature Flags +# .NET Feature Management -Feature flags provide a way for ASP.NET Core applications to turn features on or off dynamically. Developers can use feature flags in simple use cases like conditional statements to more advanced scenarios like conditionally adding routes or MVC filters. Feature flags build on top of the .NET Core configuration system. Any .NET Core configuration provider is capable of acting as the back-bone for feature flags. +The Microsoft.FeatureManagement library enables developers to use feature flags and dynamic features inside of their applications. Feature flags can be used to turn features on or off dynamically. Developers can use feature flags in simple use cases like conditional statements to more advanced scenarios like conditionally adding routes or MVC filters. Dynamic features can be used to select different variants of a feature's configuration. This enables the possibility of using one version of a feature for one set of users, and another version of the feature for the remaining users. Here are some of the benefits of using this library: @@ -8,11 +8,11 @@ Here are some of the benefits of using this library: * Low barrier-to-entry * Built on `IConfiguration` * Supports JSON file feature flag setup -* Feature Flag lifetime management +* Feature flag lifetime management * Configuration values can change in real-time, feature flags can be consistent across the entire request -* Simple to Complex Scenarios Covered +* Simple to complex scenarios covered * Toggle on/off features through declarative configuration file - * Dynamically evaluate state of feature based on call to server + * Use different variants of a feature in different circumstances * API extensions for ASP.NET Core and MVC framework * Routing * Filters @@ -20,91 +20,76 @@ Here are some of the benefits of using this library: **API Reference**: https://go.microsoft.com/fwlink/?linkid=2091700 -### Feature Flags -Feature flags are composed of two parts, a name and a list of feature-filters that are used to turn the feature on. +## Index +* [Feature Flags](./README.md#Feature-Flags) + * [Feature Flag Declaration](./README.md#Feature-Flag-Declaration) + * [Feature Filters](./README.md#Feature-Filters) + * [ASP.NET Core Integration](./README.md#ASPNET-Core-Integration) + * [Built-in Feature Filters](./README.md#Built-in-Feature-Filters) +* [Dynamic Features](./README.md#Dynamic-Features) + * [Dynamic Feature Declaration](./README.md#Dynamic-Feature-Declaration) + * [Feature Variant Assigners](./README.md#Feature-Variant-Assigners) + * [Built-in Feature Variant Assigners](./README.md#Built-in-Feature-Variant-Assigners) +* [Targeting](./README.md#Targeting) +* [Caching](./README.md#Caching) +* [Custom Feature Providers](./README.md#Custom-Feature-Providers) -### Feature Filters -Feature filters define a scenario for when a feature should be enabled. When a feature is evaluated for whether it is on or off, its list of feature-filters are traversed until one of the filters decides the feature should be enabled. At this point the feature is considered enabled and traversal through the feature filters stops. If no feature filter indicates that the feature should be enabled, then it will be considered disabled. +## Feature Flags +Feature flags can either be on or off. They are composed of two parts, a name and a list of feature-filters that are used to turn the feature on. -As an example, a Microsoft Edge browser feature filter could be designed. This feature filter would activate any features it is attached to as long as an HTTP request is coming from Microsoft Edge. - -## Registration +### Feature Flag Configuration -The .NET Core configuration system is used to determine the state of feature flags. The foundation of this system is `IConfiguration`. Any provider for IConfiguration can be used as the feature state provider for the feature flag library. This enables scenarios ranging from appsettings.json to Azure App Configuration and more. +The .NET Core configuration system is used to determine the state of features. The foundation of this system is `IConfiguration`. Any provider for IConfiguration can be used as the feature state provider for the feature management library. This enables scenarios ranging from appsettings.json to Azure App Configuration and more. ### Feature Flag Declaration -The feature management library supports appsettings.json as a feature flag source since it is a provider for .NET Core's IConfiguration system. Below we have an example of the format used to set up feature flags in a json file. +The feature management library supports appsettings.json as a feature flag source since it is a provider for .NET Core's IConfiguration system. Below we have an example of the format used to set up feature flags in a json file. The example below uses the 2.0.0 configuration schema which is supported in Microsoft.FeatureManagement version 3. For previous schema versions see the configuration [schema details](./docs/schemas/README.md). ``` JavaScript { - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - - // Define feature flags in a json file + // Define feature flags in a json configuration file "FeatureManagement": { - "FeatureT": { - "EnabledFor": [ - { - "Name": "AlwaysOn" - } - ] - }, - "FeatureU": { - "EnabledFor": [] - }, - "FeatureV": { - "EnabledFor": [ - { - "Name": "TimeWindow", - "Parameters": { - "Start": "Wed, 01 May 2019 13:59:59 GMT", - "End": "Mon, 01 July 2019 00:00:00 GMT" + "FeatureFlags": { + "FeatureT": { + "EnabledFor": [ + { + "Name": "AlwaysOn" } - } - ] + ] + }, + "FeatureU": { + "EnabledFor": [ + { + "Name": "TimeWindow", + "Parameters": { + "Start": "Wed, 01 May 2019 13:59:59 GMT", + "End": "Mon, 01 July 2019 00:00:00 GMT" + } + } + ] + } } } } ``` -The `FeatureManagement` section of the json document is used by convention to load feature flag settings. In the section above, we see that we have provided three different features. Features define their feature filters using the `EnabledFor` property. In the feature filters for `FeatureT` we see `AlwaysOn`. This feature filter is built-in and if specified will always enable the feature. The `AlwaysOn` feature filter does not require any configuration so it only has the _Name_ property. `FeatureU` has no filters in its `EnabledFor` property and thus will never be enabled. Any functionality that relies on this feature being enabled will not be accessible as long as the feature filters remain empty. However, as soon as a feature filter is added that enables the feature it can begin working. `FeatureV` specifies a feature filter named `TimeWindow`. This is an example of a configurable feature filter. We can see in the example that the filter has a parameter's property. This is used to configure the filter. In this case, the start and end times for the feature to be active are configured. +The `FeatureManagement` section of the json document is used by convention to load feature flag settings. In the section above, we see that we have provided two different features. Features define their feature filters using the `EnabledFor` property. In the feature filters for `FeatureT` we see `AlwaysOn`. This feature filter is built-in and if specified will always enable the feature. The `AlwaysOn` feature filter does not require any configuration so it only has the _Name_ property. `FeatureU` specifies a feature filter named `TimeWindow`. This is an example of a configurable feature filter. We can see in the example that the filter has a parameter's property. This is used to configure the filter. In this case, the start and end times for the feature to be active are configured. ### On/Off Declaration The following snippet demonstrates an alternative way to define a feature that can be used for on/off features. ``` JavaScript { - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - - // Define feature flags in config file + // Define feature flags in a json configuration file "FeatureManagement": { - "FeatureT": true, // On feature - "FeatureX": false // Off feature + "FeatureFlags": { + "FeatureT": true, // On feature + "FeatureX": false // Off feature + } } } ``` -### Referencing - -To make it easier to reference these feature flags in code, we recommend to define feature flag variables like below. -``` C# -// Define feature flags in an enum -public enum MyFeatureFlags -{ - FeatureT, - FeatureU, - FeatureV -} -``` - ### Service Registration Feature flags rely on .NET Core dependency injection. We can register the feature management services using standard conventions. @@ -115,12 +100,12 @@ using Microsoft.FeatureManagement.FeatureFilters; public class Startup { - public void ConfigureServices(IServiceCollection services) - { - services.AddFeatureManagement() - .AddFeatureFilter() - .AddFeatureFilter(); - } + public void ConfigureServices(IServiceCollection services) + { + services.AddFeatureManagement() + .AddFeatureFilter() + .AddFeatureFilter(); + } } ``` @@ -139,7 +124,7 @@ The basic form of feature management is checking if a feature is enabled and the … IFeatureManager featureManager; … -if (await featureManager.IsEnabledAsync(nameof(MyFeatureFlags.FeatureU))) +if (await featureManager.IsEnabledAsync("FeatureU")) { // Do something } @@ -161,11 +146,15 @@ public class HomeController : Controller } ``` +## ASP.NET Core Integration + +The feature management library provides functionality in ASP.NET Core and MVC to enable common feature flag scenarios in web applications. These capabilities are available by referencing the [Microsoft.FeatureManagement.AspNetCore](https://www.nuget.org/packages/Microsoft.FeatureManagement.AspNetCore/) NuGet package. + ### Controllers and Actions -MVC controller and actions can require that a given feature, or one of any list of features, be enabled in order to execute. This can be done by using a `FeatureGateAttribute`, which can be found in the `Microsoft.FeatureManagement.Mvc` namespace. +MVC controller and actions can require that a given feature flag, or one of any list of feature flags, be enabled in order to execute. This can be done by using a `FeatureGateAttribute`, which can be found in the `Microsoft.FeatureManagement.Mvc` namespace. ``` C# -[FeatureGate(MyFeatureFlags.FeatureX)] +[FeatureGate("FeatureX")] public class HomeController : Controller { … @@ -175,7 +164,7 @@ public class HomeController : Controller The `HomeController` above is gated by "FeatureX". "FeatureX" must be enabled before any action the `HomeController` contains can be executed. ``` C# -[FeatureGate(MyFeatureFlags.FeatureY)] +[FeatureGate("FeatureY")] public IActionResult Index() { return View(); @@ -186,7 +175,7 @@ The `Index` MVC action above requires "FeatureY" to be enabled before it can exe ### Disabled Action Handling -When an MVC controller or action is blocked because none of the features it specifies are enabled, a registered `IDisabledFeaturesHandler` will be invoked. By default, a minimalistic handler is registered which returns HTTP 404. This can be overridden using the `IFeatureManagementBuilder` when registering feature flags. +When an MVC controller or action is blocked because none of the feature flags it specifies are enabled, a registered `IDisabledFeaturesHandler` will be invoked. By default, a minimalistic handler is registered which returns HTTP 404. This can be overridden using the `IFeatureManagementBuilder` when registering feature flags. ``` C# public interface IDisabledFeaturesHandler @@ -200,41 +189,50 @@ public interface IDisabledFeaturesHandler In MVC views `` tags can be used to conditionally render content based on whether a feature is enabled or not. ``` HTML+Razor - +

This can only be seen if 'FeatureX' is enabled.

``` The `` tag requires a tag helper to work. This can be done by adding the feature management tag helper to the _ViewImports.cshtml_ file. + ``` HTML+Razor @addTagHelper *, Microsoft.FeatureManagement.AspNetCore ``` +The feature tag can also be used to show content if a feature is disabled. This is done by using the `negate` attribute. + +``` HTML+Razor + +

This can only be seen if 'FeatureX' is disabled.

+
+``` + ### MVC Filters -MVC action filters can be set up to conditionally execute based on the state of a feature. This is done by registering MVC filters in a feature aware manner. +MVC action filters can be set up to conditionally execute based on the state of a feature flag. This is done by registering MVC filters in a feature flag aware manner. The feature management pipeline supports async MVC Action filters, which implement `IAsyncActionFilter`. ``` C# services.AddMvc(o => { - o.Filters.AddForFeature(nameof(MyFeatureFlags.FeatureV)); + o.Filters.AddForFeature("FeatureV"); }); ``` -The code above adds an MVC filter named `SomeMvcFilter`. This filter is only triggered within the MVC pipeline if the feature it specifies, "FeatureV", is enabled. +The code above adds an MVC filter named `SomeMvcFilter`. This filter is only triggered within the MVC pipeline if the feature flag it specifies, "FeatureV", is enabled. ### Application building -The feature management library can be used to add application branches and middleware that execute conditionally based on feature state. +The feature management library can be used to add application branches and middleware that execute conditionally based on feature flag state. ``` C# -app.UseMiddlewareForFeature(nameof(MyFeatureFlags.FeatureU)); +app.UseMiddlewareForFeature("FeatureU"); ``` -With the above call, the application adds a middleware component that only appears in the request pipeline if the feature "FeatureU" is enabled. If the feature is enabled/disabled during runtime, the middleware pipeline can be changed dynamically. +With the above call, the application adds a middleware component that only appears in the request pipeline if the feature flag "FeatureU" is enabled. If the feature flag is enabled/disabled during runtime, the middleware pipeline can be changed dynamically. -This builds off the more generic capability to branch the entire application based on a feature. +This builds off the more generic capability to branch the entire application based on a feature flag. ``` C# app.UseForFeature(featureName, appBuilder => @@ -243,15 +241,21 @@ app.UseForFeature(featureName, appBuilder => }); ``` -## Implementing a Feature Filter +## Feature Filters + +Feature filters define a scenario for when a feature flag should be enabled. When a feature flag is evaluated for whether it is on or off, its list of feature-filters are traversed until one of the filters decides the feature flag should be enabled. At this point the feature flag is considered enabled and traversal through the feature filters stops. If no feature filter indicates that the feature flag should be enabled, then it will be considered disabled. -Creating a feature filter provides a way to enable features based on criteria that you define. To implement a feature filter, the `IFeatureFilter` interface must be implemented. `IFeatureFilter` has a single method named `EvaluateAsync`. When a feature specifies that it can be enabled for a feature filter, the `EvaluateAsync` method is called. If `EvaluateAsync` returns `true` it means the feature should be enabled. +As an example, a Microsoft Edge browser feature filter could be designed. This feature filter would activate any features it is attached to as long as an HTTP request is coming from Microsoft Edge. + +### Implementing a Feature Filter + +Creating a feature filter provides a way to enable feature flags based on criteria that you define. To implement a feature filter, the `IFeatureFilter` interface must be implemented. `IFeatureFilter` has a single method named `EvaluateAsync`. When a feature flag specifies that it can be enabled for a feature filter, the `EvaluateAsync` method is called. If `EvaluateAsync` returns `true` it means the feature flag should be enabled. Feature filters are registered by the `IFeatureManagementBuilder` when `AddFeatureManagement` is called. These feature filters have access to the services that exist within the service collection that was used to add feature flags. Dependency injection can be used to retrieve these services. ### Parameterized Feature Filters -Some feature filters require parameters to decide whether a feature should be turned on or not. For example a browser feature filter may turn on a feature for a certain set of browsers. It may be desired that Edge and Chrome browsers enable a feature, while Firefox does not. To do this a feature filter can be designed to expect parameters. These parameters would be specified in the feature configuration, and in code would be accessible via the `FeatureFilterEvaluationContext` parameter of `IFeatureFilter.EvaluateAsync`. +Some feature filters require parameters to decide whether a feature flag should be turned on or not. For example a browser feature filter may turn on a feature flag for a certain set of browsers. It may be desired that Edge and Chrome browsers enable a feature flag, while Firefox does not. To do this a feature filter can be designed to expect parameters. These parameters would be specified in the feature configuration, and in code would be accessible via the `FeatureFilterEvaluationContext` parameter of `IFeatureFilter.EvaluateAsync`. ``` C# public class FeatureFilterEvaluationContext @@ -268,7 +272,7 @@ public class FeatureFilterEvaluationContext } ``` -`FeatureFilterEvaluationContext` has a property named `Parameters`. These parameters represent a raw configuration that the feature filter can use to decide how to evaluate whether the feature should be enabled or not. To use the browser feature filter as an example once again, the filter could use `Parameters` to extract a set of allowed browsers that would have been specified for the feature and then check if the request is being sent from one of those browsers. +`FeatureFilterEvaluationContext` has a property named `Parameters`. These parameters represent a raw configuration that the feature filter can use to decide how to evaluate whether the feature flag should be enabled or not. To use the browser feature filter as an example once again, the filter could use `Parameters` to extract a set of allowed browsers that would have been specified for the feature flag and then check if the request is being sent from one of those browsers. ``` C# [FilterAlias("Browser")] @@ -303,7 +307,7 @@ This can be overridden through the use of the `FilterAliasAttribute`. A feature ### Missing Feature Filters -If a feature is configured to be enabled for a specific feature filter and that feature filter hasn't been registered, then an exception will be thrown when the feature is evaluated. The exception can be disabled by using the feature management options. +If a feature flag is configured to be enabled for a specific feature filter and that feature filter hasn't been registered, then an exception will be thrown when the feature flag is evaluated. The exception can be disabled by using the feature management options. ``` C# services.Configure(options => @@ -314,7 +318,7 @@ services.Configure(options => ### Using HttpContext -Feature filters can evaluate whether a feature should be enabled based off the properties of an HTTP Request. This is performed by inspecting the HTTP Context. A feature filter can get a reference to the HTTP Context by obtaining an `IHttpContextAccessor` through dependency injection. +Feature filters can evaluate whether a feature flag should be enabled based off the properties of an HTTP Request. This is performed by inspecting the HTTP Context. A feature filter can get a reference to the HTTP Context by obtaining an `IHttpContextAccessor` through dependency injection. ``` C# public class BrowserFilter : IFeatureFilter @@ -341,7 +345,7 @@ public void ConfigureServices(IServiceCollection services) ## Providing a Context For Feature Evaluation -In console applications there is no ambient context such as `HttpContext` that feature filters can acquire and utilize to check if a feature should be on or off. In this case, applications need to provide an object representing a context into the feature management system for use by feature filters. This is done by using `IFeatureManager.IsEnabledAsync(string featureName, TContext appContext)`. The appContext object that is provided to the feature manager can be used by feature filters to evaluate the state of a feature. +In console applications there is no ambient context such as `HttpContext` that feature filters can acquire and utilize to check if a feature should be on or off. In this case, applications need to provide an object representing a context into the feature management system for use by feature filters. This is done by using `IFeatureManager.IsEnabledAsync(string featureName, TContext appContext)`. The appContext object that is provided to the feature manager can be used by feature filters to evaluate the state of a feature flag. ``` C# MyAppContext context = new MyAppContext @@ -357,7 +361,7 @@ if (await featureManager.IsEnabledAsync(feature, context)) ### Contextual Feature Filters -Contextual feature filters implement the `IContextualFeatureFilter` interface. These special feature filters can take advantage of the context that is passed in when `IFeatureManager.IsEnabledAsync` is called. The `TContext` type parameter in `IContextualFeatureFilter` describes what context type the filter is capable of handling. This allows the developer of a contextual feature filter to describe what is required of those who wish to utilize it. Since every type is a descendant of object, a filter that implements `IContextualFeatureFilter` can be called for any provided context. To illustrate an example of a more specific contextual feature filter, consider a feature that is enabled if an account is in a configured list of enabled accounts. +Contextual feature filters implement the `IContextualFeatureFilter` interface. These special feature filters can take advantage of the context that is passed in when `IFeatureManager.IsEnabledAsync` is called. The `TContext` type parameter in `IContextualFeatureFilter` describes what context type the filter is capable of handling. This allows the developer of a contextual feature filter to describe what is required of those who wish to utilize it. Since every type is a descendant of object, a filter that implements `IContextualFeatureFilter` can be called for any provided context. To illustrate an example of a more specific contextual feature filter, consider a feature flag that is enabled if an account is in a configured list of enabled accounts. ``` C# public interface IAccountContext @@ -376,7 +380,7 @@ class AccountIdFilter : IContextualFeatureFilter } ``` -We can see that the `AccountIdFilter` requires an object that implements `IAccountContext` to be provided to be able to evalute the state of a feature. When using this feature filter, the caller needs to make sure that the passed in object implements `IAccountContext`. +We can see that the `AccountIdFilter` requires an object that implements `IAccountContext` to be provided to be able to evalute the state of a feature flag. When using this feature filter, the caller needs to make sure that the passed in object implements `IAccountContext`. **Note:** Only a single feature filter interface can be implemented by a single type. Trying to add a feature filter that implements more than a single feature filter interface will result in an `ArgumentException`. @@ -388,7 +392,7 @@ Each of the built-in feature filters have their own parameters. Here is the list #### Microsoft.Percentage -This filter provides the capability to enable a feature based on a set percentage. +This filter provides the capability to enable a feature flag based on a set percentage. ``` JavaScript "EnhancedPipeline": { @@ -405,7 +409,7 @@ This filter provides the capability to enable a feature based on a set percentag #### Microsoft.TimeWindow -This filter provides the capability to enable a feature based on a time window. If only `End` is specified, the feature will be considered on until that time. If only start is specified, the feature will be considered on at all points after that time. +This filter provides the capability to enable a feature flag based on a time window. If only `End` is specified, the feature flag will be considered on until that time. If only start is specified, the feature flag will be considered on at all points after that time. ``` JavaScript "EnhancedPipeline": { @@ -423,7 +427,7 @@ This filter provides the capability to enable a feature based on a time window. #### Microsoft.Targeting -This filter provides the capability to enable a feature for a target audience. An in-depth explanation of targeting is explained in the [targeting](./README.md#Targeting) section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature enabled. +This filter provides the capability to enable a feature flag for a target audience. An in-depth explanation of targeting is explained in the [targeting](./README.md#Targeting) section below. The filter parameters include an audience object which describes users, groups, and a default percentage of the user base that should have access to the feature flag. Each group object that is listed in the target audience must also specify what percentage of the group's members should have access. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will have the feature flag enabled. ``` JavaScript "EnhancedPipeline": { @@ -458,6 +462,254 @@ This filter provides the capability to enable a feature for a target audience. A All of the built-in feature filter alias' are in the 'Microsoft' feature filter namespace. This is to prevent conflicts with other feature filters that may share the same simple alias. The segments of a feature filter namespace are split by the '.' character. A feature filter can be referenced by its fully qualified alias such as 'Microsoft.Percentage' or by the last segment which in the case of 'Microsoft.Percentage' is 'Percentage'. +## Dynamic Features + +When new features are being added to an application there may come a time when a feature has multiple different proposed design options. A common pattern when this happens is to do some form of A/B testing. That is, provide a different version of the feature to different segments of the user base, and judge off user interaction which is better. The dynamic feature functionality contained in this library aims to proivde a simplistic, standardized method for developers to perform this form of A/B testing. + + + In the scenario above, the different proposals for the design of a feature are referred to as variants of the feature. The feature itself is referred to as a dynamic feature. The variants of a dynamic feature can have types ranging from object, to string, to integer and so on. There is no limit to the amount of variants a dynamic feature may have. A developer is free to choose what type should be returned when a variant of a dynamic feature is requested. They are also free to choose how many variants are available to select from. + +Each variant of a dynamic feature is associated with a different configuration of the feature. Additionally, each variant of a dynamic feature contains information describing under what circumstances the variant should be used. + +### Consumption + +Dynamic features are accessible through the `IDynamicFeatureManager` interface. + +``` C# +public interface IDynamicFeatureManager +{ + IAsyncEnumerable GetDynamicFeatureNamesAsync(CancellationToken cancellationToken = default); + + ValueTask GetVariantAsync(string dynamicFeature, CancellationToken cancellationToken = default); + + ValueTask GetVariantAsync(string dynamicFeature, TContext context, CancellationToken cancellationToken = default); +} +``` + +The dynamic feature manager performs a resolution process that takes the name of a feature and returns a strongly typed value to represent the variant's value. + +The following steps are performed during the retrieval of a dynamic feature's variant +1. Lookup the configuration of the specified dynamic feature to find the registered variants +2. Assign one of the registered variants to be used. +3. Resolve typed value based off of the assigned variant. + +The dynamic feature manager is made available by using the `AddFeatureManagement` call. Make sure to add any required feature variant assigners referenced by dynamic features within the application by using `AddFeatureVariantAssigner`. + +``` C# +using Microsoft.FeatureManagement; +using Microsoft.FeatureManagement.Assigners; + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddFeatureManagement() + .AddFeatureVariantAssigner(); + } +} +``` + +### Usage Example + +One possible example of when variants may be used is in a web application when there is a desire to test different visuals. In the following examples a mock of how one might assign different variants of a web page background to their users is shown. + +``` C# +// +// Modify view based off multiple possible variants +model.BackgroundUrl = featureVariantManager.GetVariantAsync("HomeBackground", cancellationToken); + +return View(model); +``` + +### Dynamic Feature Declaration + +Dynamic features can be configured in a configuration file similarly to feature flags. Instead of being defined in the `FeatureManagement:FeatureFlags` section, they are defined in the `FeatureManagement:DynamicFeatures` section. Additionally, dynamic features have the following properties. + +* Assigner: The assigner that should be used to select which variant should be used any time this feature is accessed. +* Variants: The different variants of the dynamic feature. + * Name: The name of the variant. + * Default: Whether the variant should be used if no variant could be explicitly assigned. One and only one default variant is required. + * ConfigurationReference: A reference to the configuration of the variant to be used as typed options in the application. + * AssignmentParameters: The parameters used in the assignment process to determine if this variant should be used. + +An example of a dynamic feature named "ShoppingCart" is shown below. + +``` JavaScript +{ + "FeatureManagement": + { + "DynamicFeatures": { + "ShoppingCart": { + "Assigner": "Targeting", + "Variants": [ + { + "Default": true, + "Name": "Big", + "ConfigurationReference": "ShoppingCart:Big", + "AssignmentParameters": { + "Audience": { + "Users": [ + "Alec", + ], + "Groups": [ + ] + } + } + }, + { + "Name": "Small", + "ConfigurationReference": "ShoppingCart:Small", + "AssignmentParameters": { + "Audience": { + "Users": [ + ], + "Groups": [ + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 30 + } + } + } + ] + } + } + }, + "ShoppingCart": { + "Big": { + "Size": 400, + "Color": "green" + }, + "Small": { + "Size": 150, + "Color": "gray" + } + } +} +``` + +In the example above we see the declaration of a dynamic feature in a json configuration file. The dynamic feature is defined in the `FeatureManagement:DynamicFeatures` section of configuration. The name of this dynamic feature is `ShoppingCart`. A dynamic feature must declare a feature variant assigner that should be used to select a variant when requested. In this case the built-in `Targeting` feature variant assigner is used. The dynamic feature has two different variants that are available to the application. One variant is named `Big` and the other is named `Small`. Each variant contains a configuration reference denoted by the `ConfigurationReference` property. The configuration reference is a pointer to a section of application configuration that contains the options that should be used for that variant. The variant also contains assignment parameters denoted by the `AssignmentParameters` property. The assignment parameters are used by the assigner associated with the dynamic feature. The assigner reads the assignment parameters at run time when a variant of the dynamic feature is requested to choose which variant should be returned. + +An application that is configured with this `ShoppingCart` dynamic feature may request the value of a variant of the feature at runtime through the use of `IDynamicFeatureManager.GetVariantAsync`. The dynamic feature uses targeting for [variant assignment](./README.md#Feature-Variant-Assignment) so each of the variants' assignment parameters specify a target audience that should receive the variant. For a walkthrough of how the targeting assigner would choose a variant in this scenario reference the [Microsoft.Targeting Assigner](./README.md#MicrosoftTargeting-Feature-Variant-Assigner) section. When the feature manager chooses one of the variants it resolves the value of the variant by resolving the configuration reference declared in the variant. The example above includes the configuration that is referenced by the `ConfigurationReference` of each variant. + +### Feature Variant Assigners +A feature variant assigner is a component that uses contextual information within an application to decide which feature variant should be chosen when a variant of a dynamic feature is requested. + +### Feature Variant Assignment + +When requesting the value of a dynamic feature the feature manager needs to determine which variant of the feature should be used. The act of choosing which variant should be used is called assignment. A built-in method of assignment is provided that allows the variants of a dynamic features to be assigned to segments of an application's audience. This is the same [targeting](./README.md#MicrosoftTargeting-Feature-Variant-Assigner) strategy introduced by the targeting feature filter. + +To perform assignment the feature manager uses components known as feature variant assigners. Feature variant assigners have the job of choosing which of the variants of a dynamic feature should be used when the value of a dynamic feature is requested. Each variant of a dynamic feature declares assignment parameters so that when an assigner is invoked the assigner can tell under which conditions each variant should be selected. It is possible that an assigner is unable to choose between the list of available variants based off of their configured assignment parameters. In this case the feature manager chooses the **default variant**. The default variant is a variant that is marked explicitly as default. It is required to have a default variant when configuring a dynamic feature in order to handle the possibility that an assigner is not able to select a variant of a dynamic feature. + +### Custom Assignment + +There may come a time when custom criteria is needed to decide which variant of a feature should be assigned when a feature is referenced. This is made possible by an extensibility model that allows the act of assignment to be overriden. Every feature registered in the feature management system that uses feature variants specifies what assigner should be used to choose a variant. + + +``` C# + public interface IFeatureVariantAssigner : IFeatureVariantAssignerMetadata + { + /// + /// Assign a variant of a feature to be used based off of customized criteria. + /// + /// Information provided by the system to be used during the assignment process. + /// The cancellation token to cancel the operation. + /// The variant that should be assigned for a given feature. + ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken cancellationToken); + } +} +``` + +An example implementation can be found in [this example](./examples/CustomAssignmentConsoleApp/RecurringAssigner.cs). + +### Built-In Feature Variant Assigners + +There is a built-in feature variant assigner that uses targeting that comes with the `Microsoft.FeatureManagement` package. This assigner is not added automatically, but it can be referenced and registered as soon as the package is registered. + +#### Microsoft.Targeting Feature Variant Assigner + +This feature variant assigner provides the capability to assign the variants of a dynamic feature to targeted audiences. An in-depth explanation of targeting is explained in the [targeting](./README.md#Targeting) section. + +The assignment parameters used by the targeting feature variant assigner include an audience object which describes users, groups, and a default percentage of the user base that should receive the associated variant. Each group object that is listed in the target audience must also specify what percentage of the group's members should have receive the variant. If a user is specified in the users section directly, or if the user is in the included percentage of any of the group rollouts, or if the user falls into the default rollout percentage then that user will receive the associated variant. + +``` JavaScript +"ShoppingCart": { + "Assigner": "Targeting", + "Variants": [ + { + "Default": true, + "Name": "Big", + "ConfigurationReference": "ShoppingCart:Big", + "AssignmentParameters": { + "Audience": { + "Users": [ + "Alec", + ], + "Groups": [ + { + "Name": "Ring0", + "RolloutPercentage": 100 + }, + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ] + } + } + }, + { + "Name": "Small", + "ConfigurationReference": "ShoppingCart:Small", + "AssignmentParameters": { + "Audience": { + "Users": [ + "Susan", + ], + "Groups": [ + { + "Name": "Ring1", + "RolloutPercentage": 50 + } + ], + "DefaultRolloutPercentage": 80 + } + } + } + ] +} +``` + +Based on the configured audiences for the variants included in this feature, if the application is executing under the context of a user named `Alec` then the value of the `Big` variant will be returned. If the application is executing under the context of a user named `Susan` then the value of the `Small` variant will be returned. If a user match does not occur, then group matches are evaluated. If the application is executing under the context of a user in the group `Ring0` then the `Big` variant will be returned. If the user's group is `Ring1` instead, then the user has a 50% chance between being assigned to `Big` or `Small`. If there is not user match nor group match then the default rollout percentage is used. In this case, 80% of unmatched users will get the `Small` variant leaving the other 20% to get the `Big` variant since it is marked as the `Default`. + +Example usage of this assigner can be found in the [FeatureFlagDemo example](./examples/FeatureFlagDemo/Startup.cs#L63). + +When using the targeting feature variant assigner, make sure to register it as well as an implementation of [ITargetingContextAccessor](./README.md#ITargetingContextAccessor). + +``` C# +services.AddSingleton(); + +services.AddFeatureManagement(); + .AddFeatureVariantAssigner(); +``` + +### Variant Resolution + +When a variant of a dynamic feature has been chosen, the feature management system needs to resolve the configuration reference associated with that variant. A feature variant references configuration through its `ConfigurationReference` property. In the "[Configuring a Dynamic Feature](./README.md#Configuring-a-Dynamic-Feature)" section we see a dynamic feature named "ShoppingCart". The first variant of the feature, named "Big", has a configuration reference to the `ShoppingCart:Big` configuration section. The referenced section is shown below. + +``` Javascript + "ShoppingCart": { + "Big": { + "Size": 400, + "Color": "green" + } + } +``` + +The feature management system resolves the configuration reference and binds the resolved configuration section to the type specfied when a variant of a dynamic feature is requested. This is performed by an implementation of the `IFeatureVariantOptionsResolver`. By providing a custom implementation of `IFeatureVariantOptionsResolver`, a developer can resolve configuration references from sources other than configuration. + ## Targeting Targeting is a feature management strategy that enables developers to progressively roll out new features to their user base. The strategy is built on the concept of targeting a set of users known as the target _audience_. An audience is made up of specific users, groups, and a designated percentage of the entire user base. The groups that are included in the audience can be broken down further into percentages of their total members. @@ -481,7 +733,7 @@ To begin using the `TargetingFilter` in an application it must be added to the a The implementation type used for the `ITargetingContextAccessor` service must be implemented by the application that is using the targeting filter. Here is an example setting up feature management in a web application to use the `TargetingFilter` with an implementation of `ITargetingContextAccessor` called `HttpContextTargetingContextAccessor`. -``` +``` C# services.AddSingleton(); services.AddFeatureManagement(); @@ -499,14 +751,14 @@ An example that extracts targeting context information from the application's HT The targeting filter relies on a targeting context to evaluate whether a feature should be turned on. This targeting context contains information such as what user is currently being evaluated, and what groups the user in. In console applications there is typically no ambient context available to flow this information in to the targeting filter, thus it must be passed directly when `FeatureManager.IsEnabledAsync` is called. This is supported through the use of the `ContextualTargetingFilter`. Applications that need to float the targeting context into the feature manager should use this instead of the `TargetingFilter.` -``` +``` C# services.AddFeatureManagement() .AddFeatureFilter(); ``` Since `ContextualTargetingFilter` is an [`IContextualTargetingFilter`](./README.md#Contextual-Feature-Filters), an implementation of `ITargetingContext` must be passed in to `IFeatureManager.IsEnabledAsync` for it to be able to evaluate and turn a feature on. -``` +``` C# IFeatureManager fm; … // userId and groups defined somewhere earlier in application @@ -527,42 +779,48 @@ An example that uses the `ContextualTargetingFilter` in a console application is Options are available to customize how targeting evaluation is performed across all features. These options can be configured when setting up feature management. -``` +``` C# services.Configure(options => { options.IgnoreCase = true; }); ``` +### Targeting in Dynamic Features + +The concept of targeting can be extended to dynamic features. Instead of targeting an audience to see a feature as enabled, the variants of a dynamic feature can be configured to target different audiences. For an in depth view of how this can be done see the [targeting feature variant assigner](./README.md#MicrosoftTargeting-Feature-Variant-Assigner) section. + ## Caching Feature state is provided by the IConfiguration system. Any caching and dynamic updating is expected to be handled by configuration providers. The feature manager asks IConfiguration for the latest value of a feature's state whenever a feature is checked to be enabled. ### Snapshot -There are scenarios which require the state of a feature to remain consistent during the lifetime of a request. The values returned from the standard `IFeatureManager` may change if the `IConfiguration` source which it is pulling from is updated during the request. This can be prevented by using `IFeatureManagerSnapshot`. `IFeatureManagerSnapshot` can be retrieved in the same manner as `IFeatureManager`. `IFeatureManagerSnapshot` implements the interface of `IFeatureManager`, but it caches the first evaluated state of a feature during a request and will return the same state of a feature during its lifetime. +There are scenarios which require the state of a feature to remain consistent during the lifetime of a request. The values returned from the standard `IFeatureManager` may change if the `IConfiguration` source which it is pulling from is updated during the request. This can be prevented by using `IFeatureManagerSnapshot`. `IFeatureManagerSnapshot` can be retrieved in the same manner as `IFeatureManager`. `IFeatureManagerSnapshot` implements the interface of `IFeatureManager`, but it caches the first evaluated state of a feature during a request and will return the same state of a feature during its lifetime. Symmetric functionality is available for dynamic features through the use of `IDynamicFeatureManagerSnapshot`. ## Custom Feature Providers Implementing a custom feature provider enable developers to pull feature flags from sources such as a database or a feature management service. The included feature provider that is used by default pulls feature flags from .NET Core's configuration system. This allows for features to be defined in an [appsettings.json](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#jcp) file or in configuration providers like [Azure App Configuration](https://docs.microsoft.com/en-us/azure/azure-app-configuration/quickstart-feature-flag-aspnet-core?tabs=core2x). This behavior can be substituted to provide complete control of where feature definitions are read from. -To customize the loading of feature definitions, one must implement the `IFeatureDefinitionProvider` interface. +To customize the loading of feature definitions, one must implement the `IFeatureFlagDefinitionProvider` interface. -``` -public interface IFeatureDefinitionProvider +``` C# +public interface IFeatureFlagDefinitionProvider { - Task GetFeatureDefinitionAsync(string featureName); + Task GetFeatureFlagDefinitionAsync(string featureName, CancellationToken cancellationToken = default); - IAsyncEnumerable GetAllFeatureDefinitionsAsync(); + IAsyncEnumerable GetAllFeatureFlagDefinitionsAsync(CancellationToken cancellationToken = default); } ``` -To use an implementation of `IFeatureDefinitionProvider` it must be added into the service collection before adding feature management. The following example adds an implementation of `IFeatureDefinitionProvider` named `InMemoryFeatureDefinitionProvider`. +To use an implementation of `IFeatureFlagDefinitionProvider` it must be added into the service collection before adding feature management. The following example adds an implementation of `IFeatureFlagDefinitionProvider` named `InMemoryFeatureDefinitionProvider`. -``` -services.AddSingleton() +``` C# +services.AddSingleton() .AddFeatureManagement() ``` +It is also possible to provide custom dynamic feature definitions. This is done by implementing the `IDynamicFeatureDefinitionProvider` interface and registering it as mentioned above. + # Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/docs/schemas/FeatureManagement.v1.0.0.json b/docs/schemas/FeatureManagement.v1.0.0.json new file mode 100644 index 00000000..83f6a1df --- /dev/null +++ b/docs/schemas/FeatureManagement.v1.0.0.json @@ -0,0 +1,75 @@ +{ + "$schema":"http://json-schema.org/draft-07/schema#", + "$id":"http://azconfig.io/schemas/FeatureManagement-DotNet/v1.0.0/FeatureManagement.json", + "title":"Decalaration of features in the Microsoft.FeatureManagement library.", + "definitions": { + "FeatureFlag": { + "type":"object", + "properties": { + "EnabledFor": { + "type":"array", + "items": { + "type":"object", + "required":[ + "Name" + ], + "properties": { + "Name": { + "type":"string" + }, + "Parameters": { + "type":"object", + "patternProperties": { + "^.*$": { + "anyOf":[ + { + "type":"string" + }, + { + "type":"null" + }, + { + "type":"object" + }, + { + "type":"number" + }, + { + "type":"array" + }, + { + "type":"boolean" + } + ] + } + }, + "additionalProperties":false + } + } + } + } + } + } + }, + "type":"object", + "required":[ + ], + "properties": { + "FeatureManagement": { + "type":"object", + "patternProperties": { + "^.*$": { + "anyOf":[ + { + "$ref":"#/definitions/FeatureFlag" + }, + { + "type":"boolean" + } + ] + } + }, + "additionalProperties":false + } + } +} diff --git a/docs/schemas/FeatureManagement.v2.0.0.json b/docs/schemas/FeatureManagement.v2.0.0.json new file mode 100644 index 00000000..a01338b6 --- /dev/null +++ b/docs/schemas/FeatureManagement.v2.0.0.json @@ -0,0 +1,150 @@ +{ + "$schema":"http://json-schema.org/draft-07/schema#", + "$id":"http://azconfig.io/schemas/FeatureManagement-DotNet/v2.0.0/FeatureManagement.json", + "title":"Decalaration of features in the Microsoft.FeatureManagement library.", + "definitions": { + "FeatureFlag": { + "type":"object", + "properties": { + "EnabledFor": { + "type":"array", + "items": { + "type":"object", + "required":[ + "Name" + ], + "properties": { + "Name": { + "type":"string" + }, + "Parameters": { + "type":"object", + "patternProperties": { + "^.*$": { + "anyOf":[ + { + "type":"string" + }, + { + "type":"null" + }, + { + "type":"object" + }, + { + "type":"number" + }, + { + "type":"array" + }, + { + "type":"boolean" + } + ] + } + }, + "additionalProperties":false + } + } + } + } + } + }, + "DynamicFeature": { + "type":"object", + "required": [ + "Assigner", + "Variants" + ], + "properties": { + "Assigner": { + "type":"string" + }, + "Variants": { + "type":"array", + "items": { + "type":"object", + "required":[ + "Name", + "ConfigurationReference" + ], + "properties": { + "Default": { + "type":"boolean" + }, + "Name": { + "type":"string" + }, + "ConfigurationReference": { + "type":"string" + }, + "AssignmentParameters": { + "type":"object", + "patternProperties": { + "^.*$": { + "anyOf":[ + { + "type":"string" + }, + { + "type":"null" + }, + { + "type":"object" + }, + { + "type":"number" + }, + { + "type":"array" + }, + { + "type":"boolean" + } + ] + } + }, + "additionalProperties":false + } + } + } + } + } + } + }, + "type":"object", + "required":[ + ], + "properties": { + "FeatureManagement": { + "type":"object", + "properties": { + "FeatureFlags": { + "type":"object", + "patternProperties": { + "^.*$": { + "anyOf":[ + { + "$ref":"#/definitions/FeatureFlag" + }, + { + "type":"boolean" + } + ] + } + }, + "additionalProperties":false + }, + "DynamicFeatures": { + "type":"object", + "patternProperties": { + "^.*$": { + "$ref":"#/definitions/DynamicFeature" + } + }, + "additionalProperties":false + } + } + } + } +} diff --git a/docs/schemas/README.md b/docs/schemas/README.md new file mode 100644 index 00000000..2b94065e --- /dev/null +++ b/docs/schemas/README.md @@ -0,0 +1,17 @@ +# Configuration Schemas + +This folder contains the schemas for the configuration used by the Microsoft.FeatureManagement library. + +# 1.0.0 + +The [1.0.0 schema](./FeatureManagement.v1.0.0.json) is supported by Microsoft.FeatureManagement version 1.x - 3.x. + +* Allows feature flags to be defined. + +# 2.0.0 + +The [2.0.0 schema](./FeatureManagement.v2.0.0.json) is supported by Microsoft.FeatureManagement version 3.x. + +* Allows dynamic features to be defined. +* Uses a more explicit path to define feature flags. + * "FeatureManagement:FeatureFlags:{flagName}" instead of "FeatureManagement:{flagName}".