Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 26 additions & 26 deletions Microsoft.FeatureManagement.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31825.309
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureFlagDemo", "examples\FeatureFlagDemo\FeatureFlagDemo.csproj", "{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.FeatureManagement", "src\Microsoft.FeatureManagement\Microsoft.FeatureManagement.csproj", "{ED1A3494-6D5B-4B27-BA9C-8BAF93E36955}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.FeatureManagement", "tests\Tests.FeatureManagement\Tests.FeatureManagement.csproj", "{FDBB27BA-C5BA-48A7-BA9B-63159943EA9F}"
Expand All @@ -15,24 +13,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.FeatureManagement
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "examples\ConsoleApp\ConsoleApp.csproj", "{E50FB931-7A42-440E-AC47-B8DFE5E15394}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.FeatureManagement.AspNetCore", "tests\Tests.FeatureManagement.AspNetCore\Tests.FeatureManagement.AspNetCore.csproj", "{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "examples\ConsoleApp\ConsoleApp.csproj", "{7B98D293-F270-423E-A9A6-0D388E903AE9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TargetingConsoleApp", "examples\TargetingConsoleApp\TargetingConsoleApp.csproj", "{6558C21E-CF20-4278-AA08-EB9D1DF29D66}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RazorPages", "examples\RazorPages\RazorPages.csproj", "{36DBB413-D9CA-4C56-AE5B-EAEA4C344DB3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorPages", "examples\RazorPages\RazorPages.csproj", "{BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeatureFlagDemo", "examples\FeatureFlagDemo\FeatureFlagDemo.csproj", "{DACAB624-4611-42E8-844C-529F93A54980}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.FeatureManagement.AspNetCore", "tests\Tests.FeatureManagement.AspNetCore\Tests.FeatureManagement.AspNetCore.csproj", "{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TargetingConsoleApp", "examples\TargetingConsoleApp\TargetingConsoleApp.csproj", "{283D3EBB-4716-4F1D-BA51-A435F7E2AB82}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F}.Release|Any CPU.Build.0 = Release|Any CPU
{ED1A3494-6D5B-4B27-BA9C-8BAF93E36955}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED1A3494-6D5B-4B27-BA9C-8BAF93E36955}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED1A3494-6D5B-4B27-BA9C-8BAF93E36955}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand All @@ -45,33 +41,37 @@ Global
{CFD3E549-2E24-490D-A7F6-F95E56A81092}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CFD3E549-2E24-490D-A7F6-F95E56A81092}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CFD3E549-2E24-490D-A7F6-F95E56A81092}.Release|Any CPU.Build.0 = Release|Any CPU
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Release|Any CPU.Build.0 = Release|Any CPU
{6558C21E-CF20-4278-AA08-EB9D1DF29D66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6558C21E-CF20-4278-AA08-EB9D1DF29D66}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6558C21E-CF20-4278-AA08-EB9D1DF29D66}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6558C21E-CF20-4278-AA08-EB9D1DF29D66}.Release|Any CPU.Build.0 = Release|Any CPU
{BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2}.Release|Any CPU.Build.0 = Release|Any CPU
{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4}.Release|Any CPU.Build.0 = Release|Any CPU
{7B98D293-F270-423E-A9A6-0D388E903AE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B98D293-F270-423E-A9A6-0D388E903AE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B98D293-F270-423E-A9A6-0D388E903AE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B98D293-F270-423E-A9A6-0D388E903AE9}.Release|Any CPU.Build.0 = Release|Any CPU
{36DBB413-D9CA-4C56-AE5B-EAEA4C344DB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36DBB413-D9CA-4C56-AE5B-EAEA4C344DB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36DBB413-D9CA-4C56-AE5B-EAEA4C344DB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36DBB413-D9CA-4C56-AE5B-EAEA4C344DB3}.Release|Any CPU.Build.0 = Release|Any CPU
{DACAB624-4611-42E8-844C-529F93A54980}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DACAB624-4611-42E8-844C-529F93A54980}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DACAB624-4611-42E8-844C-529F93A54980}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DACAB624-4611-42E8-844C-529F93A54980}.Release|Any CPU.Build.0 = Release|Any CPU
{283D3EBB-4716-4F1D-BA51-A435F7E2AB82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{283D3EBB-4716-4F1D-BA51-A435F7E2AB82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{283D3EBB-4716-4F1D-BA51-A435F7E2AB82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{283D3EBB-4716-4F1D-BA51-A435F7E2AB82}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{FDBB27BA-C5BA-48A7-BA9B-63159943EA9F} = {8ED6FFEE-4037-49A2-9709-BC519C104A90}
{E50FB931-7A42-440E-AC47-B8DFE5E15394} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{6558C21E-CF20-4278-AA08-EB9D1DF29D66} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{BA29A1BB-81D5-4EB1-AF37-6ECF64AF27E2} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{FC0DC3E2-5646-4AEC-A7DB-2D6167BC3BB4} = {8ED6FFEE-4037-49A2-9709-BC519C104A90}
{7B98D293-F270-423E-A9A6-0D388E903AE9} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{36DBB413-D9CA-4C56-AE5B-EAEA4C344DB3} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{DACAB624-4611-42E8-844C-529F93A54980} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
{283D3EBB-4716-4F1D-BA51-A435F7E2AB82} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD}
Expand Down
48 changes: 20 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ In the above example, `FeatureW` specifies a `RequirementType` of `All`, meaning

### Status


`Status` is an optional property of a feature flag that controls how a flag's enabled state is evaluated. By default, the status of a flag is `Conditional`, meaning that feature filters should be evaluated to determine if the flag is enabled. If the `Status` of a flag is set to `Disabled` then feature filters are not evaluated and the flag is always considered to be disabled.


Expand All @@ -158,21 +157,7 @@ In the above example, `FeatureW` specifies a `RequirementType` of `All`, meaning
```

In this example, even though the `AlwaysOn` filter would normally always make the feature enabled, the `Status` property is set to `Disabled`, so this feature will always be disabled.

### 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.
Expand All @@ -194,7 +179,6 @@ public class Startup

This tells the feature manager to use the "FeatureManagement" section from the configuration for feature flag settings. It also registers two built-in feature filters named `PercentageFilter` and `TimeWindowFilter`. When filters are referenced in feature flag settings (appsettings.json) the _Filter_ part of the type name can be omitted.


**Advanced:** The feature manager looks for feature definitions in a configuration section named "FeatureManagement". If the "FeatureManagement" section does not exist, it falls back to the root of the provided configuration.

## Consumption
Expand All @@ -207,7 +191,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("FeatureX"))
{
// Do something
}
Expand Down Expand Up @@ -236,7 +220,7 @@ The feature management library provides functionality in ASP.NET Core and MVC to
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.

``` C#
[FeatureGate(MyFeatureFlags.FeatureX)]
[FeatureGate("FeatureX")]
public class HomeController : Controller
{
Expand All @@ -246,14 +230,14 @@ 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("FeatureX")]
public IActionResult Index()
{
return View();
}
```

The `Index` MVC action above requires "FeatureY" to be enabled before it can execute.
The `Index` MVC action above requires "FeatureX" to be enabled before it can execute.

### Disabled Action Handling

Expand All @@ -271,11 +255,19 @@ public interface IDisabledFeaturesHandler
In MVC views `<feature>` tags can be used to conditionally render content based on whether a feature is enabled or not.

``` HTML+Razor
<feature name=@nameof(MyFeatureFlags.FeatureX)>
<feature name="FeatureX">
<p>This can only be seen if 'FeatureX' is enabled.</p>
</feature>
```

You can also negate the tag helper evaluation to display content when a feature or set of features are disabled. By setting `negate="true"` in the example below, the content is only rendered if `FeatureX` is disabled.

``` HTML+Razor
<feature negate="true" name="FeatureX">
<p>This can only be seen if 'FeatureX' is disabled.</p>
</feature>
```

The `<feature>` 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
Expand All @@ -289,17 +281,17 @@ The feature management pipeline supports async MVC Action filters, which impleme
``` C#
services.AddMvc(o =>
{
o.Filters.AddForFeature<SomeMvcFilter>(nameof(MyFeatureFlags.FeatureV));
o.Filters.AddForFeature<SomeMvcFilter>("FeatureX");
});
```

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 it specifies, "FeatureX", is enabled.

### Razor Pages
MVC Razor pages 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.

``` C#
[FeatureGate(MyFeatureFlags.FeatureU)]
[FeatureGate("FeatureX")]
public class IndexModel : PageModel
{
public void OnGet()
Expand All @@ -308,7 +300,7 @@ public class IndexModel : PageModel
}
```

The code above sets up a Razor page to require the "FeatureU" to be enabled. If the feature is not enabled, the page will generate an HTTP 404 (NotFound) result.
The code above sets up a Razor page to require the "FeatureX" to be enabled. If the feature is not enabled, the page will generate an HTTP 404 (NotFound) result.

When used on Razor pages, the `FeatureGateAttribute` must be placed on the page handler type. It cannot be placed on individual handler methods.

Expand All @@ -317,10 +309,10 @@ When used on Razor pages, the `FeatureGateAttribute` must be placed on the page
The feature management library can be used to add application branches and middleware that execute conditionally based on feature state.

``` C#
app.UseMiddlewareForFeature<ThirdPartyMiddleware>(nameof(MyFeatureFlags.FeatureU));
app.UseMiddlewareForFeature<ThirdPartyMiddleware>("FeatureX");
```

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 "FeatureX" is enabled. If the feature 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.

Expand Down
11 changes: 3 additions & 8 deletions examples/ConsoleApp/AccountServiceContext.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Consoto.Banking.AccountService.FeatureFilters;

namespace Consoto.Banking.AccountService
class AccountServiceContext : IAccountContext
{
class AccountServiceContext : IAccountContext
{
public string AccountId { get; set; }
}
}
public string AccountId { get; set; }
}
5 changes: 3 additions & 2 deletions examples/ConsoleApp/ConsoleApp.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Consoto.Banking.AccountService</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand Down
33 changes: 13 additions & 20 deletions examples/ConsoleApp/FeatureFilters/AccountIdFilter.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
using Consoto.Banking.AccountService.FeatureFilters;
using Microsoft.Extensions.Configuration;
using Microsoft.FeatureManagement;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Consoto.Banking.AccountService.FeatureManagement
/// <summary>
/// A filter that uses the feature management context to ensure that the current task has the notion of an account id, and that the account id is allowed.
/// This filter will only be executed if an object implementing <see cref="IAccountContext"/> is passed in during feature evaluation.
/// </summary>
[FilterAlias("AccountId")]
class AccountIdFilter : IContextualFeatureFilter<IAccountContext>
{
/// <summary>
/// A filter that uses the feature management context to ensure that the current task has the notion of an account id, and that the account id is allowed.
/// This filter will only be executed if an object implementing <see cref="IAccountContext"/> is passed in during feature evaluation.
/// </summary>
[FilterAlias("AccountId")]
class AccountIdFilter : IContextualFeatureFilter<IAccountContext>
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext)
{
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext)
if (string.IsNullOrEmpty(accountContext?.AccountId))
{
if (string.IsNullOrEmpty(accountContext?.AccountId))
{
throw new ArgumentNullException(nameof(accountContext));
}
throw new ArgumentNullException(nameof(accountContext));
}

var allowedAccounts = new List<string>();
var allowedAccounts = new List<string>();

featureEvaluationContext.Parameters.Bind("AllowedAccounts", allowedAccounts);
featureEvaluationContext.Parameters.Bind("AllowedAccounts", allowedAccounts);

return Task.FromResult(allowedAccounts.Contains(accountContext.AccountId));
}
return Task.FromResult(allowedAccounts.Contains(accountContext.AccountId));
}
}
7 changes: 2 additions & 5 deletions examples/ConsoleApp/FeatureFilters/IAccountContext.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
namespace Consoto.Banking.AccountService.FeatureFilters
public interface IAccountContext
{
public interface IAccountContext
{
string AccountId { get; }
}
string AccountId { get; }
}
Loading