Skip to content

Commit dd7b9f6

Browse files
authored
Merge pull request #26 from microsoft/dev
Update master for release.
2 parents c6ff04d + 422bd86 commit dd7b9f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+891
-256
lines changed

.pipelines/pipeline.user.windows.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ restore:
1010
- !!defaultcommand
1111
name: 'Restore'
1212
command: 'build.cmd'
13-
arguments: 'RestoreOnly'
13+
arguments: '-RestoreOnly'
1414

1515
build:
1616
commands:

Microsoft.FeatureManagement.sln

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.28119.50
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29011.400
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureFlagDemo", "examples\FeatureFlagDemo\FeatureFlagDemo.csproj", "{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F}"
77
EndProject
@@ -13,6 +13,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8ED6FFEE
1313
EndProject
1414
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.FeatureManagement.AspNetCore", "src\Microsoft.FeatureManagement.AspNetCore\Microsoft.FeatureManagement.AspNetCore.csproj", "{CFD3E549-2E24-490D-A7F6-F95E56A81092}"
1515
EndProject
16+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}"
17+
EndProject
18+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "examples\ConsoleApp\ConsoleApp.csproj", "{E50FB931-7A42-440E-AC47-B8DFE5E15394}"
19+
EndProject
1620
Global
1721
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1822
Debug|Any CPU = Debug|Any CPU
@@ -35,12 +39,18 @@ Global
3539
{CFD3E549-2E24-490D-A7F6-F95E56A81092}.Debug|Any CPU.Build.0 = Debug|Any CPU
3640
{CFD3E549-2E24-490D-A7F6-F95E56A81092}.Release|Any CPU.ActiveCfg = Release|Any CPU
3741
{CFD3E549-2E24-490D-A7F6-F95E56A81092}.Release|Any CPU.Build.0 = Release|Any CPU
42+
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43+
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Debug|Any CPU.Build.0 = Debug|Any CPU
44+
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Release|Any CPU.ActiveCfg = Release|Any CPU
45+
{E50FB931-7A42-440E-AC47-B8DFE5E15394}.Release|Any CPU.Build.0 = Release|Any CPU
3846
EndGlobalSection
3947
GlobalSection(SolutionProperties) = preSolution
4048
HideSolutionNode = FALSE
4149
EndGlobalSection
4250
GlobalSection(NestedProjects) = preSolution
51+
{E58A64A6-BE10-4D7A-AAB8-C3E2925CB32F} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
4352
{FDBB27BA-C5BA-48A7-BA9B-63159943EA9F} = {8ED6FFEE-4037-49A2-9709-BC519C104A90}
53+
{E50FB931-7A42-440E-AC47-B8DFE5E15394} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
4454
EndGlobalSection
4555
GlobalSection(ExtensibilityGlobals) = postSolution
4656
SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD}

README.md

+52-19
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ Here are some of the benefits of using this library:
1010
* Supports JSON file feature flag setup
1111
* Feature Flag lifetime management
1212
* Configuration values can change in real-time, feature flags can be consistent across the entire request
13+
* Simple to Complex Scenarios Covered
14+
* Toggle on/off features through declarative configuration file
15+
* Dynamically evaluate state of feature based on call to server
1316
* API extensions for ASP.Net Core and MVC framework
1417
* Routing
1518
* Filters
@@ -128,13 +131,13 @@ This tells the feature manager to use the "FeatureManagement" section from the c
128131
The simplest use case for feature flags is to do a conditional check for whether a feature is enabled to take different paths in code. The uses cases grow from there as the feature flag API begins to offer extensions into ASP.Net Core.
129132

130133
### Feature Check
131-
The basic form of feature management is checking if a feature is enabled and then performing actions based on the result. This is done through the `IFeatureManager`'s `IsEnabled` method.
134+
The basic form of feature management is checking if a feature is enabled and then performing actions based on the result. This is done through the `IFeatureManager`'s `IsEnabledAsync` method.
132135

133136
```
134137
...
135138
IFeatureManager featureManager;
136139
...
137-
if (featureManager.IsEnabled(nameof(MyFeatureFlags.FeatureU)))
140+
if (await featureManager.IsEnabledAsync(nameof(MyFeatureFlags.FeatureU)))
138141
{
139142
// Do something
140143
}
@@ -207,7 +210,8 @@ The `<feature>` tag requires a tag helper to work. This can be done by adding th
207210

208211
### MVC Filters
209212

210-
MVC 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.
213+
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.
214+
The feature management pipeline supports async MVC Action filters, which implement `IAsyncActionFilter`.
211215

212216
```
213217
services.AddMvc(o =>
@@ -218,18 +222,6 @@ services.AddMvc(o =>
218222

219223
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.
220224

221-
### Routing
222-
Certain routes may expose application capabilites that are gated by features. These routes can be dynamically registered and exposed based on whether a feature is enabled.
223-
224-
```
225-
app.UseMvc(routes =>
226-
{
227-
routes.MapRouteForFeature(nameof(MyFeatureFlags.Beta), "betaDefault" /* route name */, "{controller=Beta}/{action=Index}/{id?}"
228-
});
229-
```
230-
231-
The route above exposes beta functionality of some application that is gated behind the "Beta" feature flag. The state of this feature is evaluated on every request, which means routes can be exposed to a subset of requests and or users. Furthermore, routes can dynamically be added or removed based on the toggling of feature states.
232-
233225
### Application building
234226

235227
The feature management library can be used to add application branches and middleware that execute conditionally based on feature state.
@@ -251,13 +243,13 @@ app.UseForFeature(featureName, appBuilder =>
251243

252244
## Implementing a Feature Filter
253245

254-
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 `Evaluate`. When a feature specifies that it can be enabled for a feature filter, the `Evaluate` method is called. If `Evaluate` returns `true` it means the feature should be enabled.
246+
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.
255247

256248
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.
257249

258250
### Parameterized Feature Filters
259251

260-
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.Evaluate`.
252+
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`.
261253

262254
```
263255
public class FeatureFilterEvaluationContext
@@ -282,7 +274,7 @@ Some feature filters require parameters to decide whether a feature should be tu
282274
{
283275
... Removed for example
284276
285-
public bool Evaluate(FeatureFilterEvaluationContext context)
277+
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
286278
{
287279
BrowserFilterSettings settings = context.Parameters.Get<BrowserFilterSettings>() ?? new BrowserFilterSettings();
288280
@@ -333,6 +325,46 @@ public void ConfigureServices(IServiceCollection services)
333325
}
334326
```
335327

328+
## Providing a Context For Feature Evaluation
329+
330+
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<TContext>(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.
331+
332+
```
333+
MyAppContext context = new MyAppContext
334+
{
335+
AccountId = current.Id;
336+
}
337+
338+
if (featureManager.IsEnabled(feature, context))
339+
{
340+
}
341+
```
342+
343+
### Contextual Feature Filters
344+
345+
Contextual feature filters implement the `IContextualFeatureFilter<TContext>` interface. These special feature filters can take advantage of the context that is passed in when `IFeatureManager.IsEnabledAsync<TContext>` is called. The `TContext` type parameter in `IContextualFeatureFilter<TContext>` 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<object>` 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.
346+
347+
```
348+
public interface IAccountContext
349+
{
350+
string AccountId { get; set; }
351+
}
352+
353+
[FilterAlias("AccountId")]
354+
class AccountIdFilter : IContextualFeatureFilter<IAccountContext>
355+
{
356+
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountId)
357+
{
358+
//
359+
// Evaluate if the feature should be on with the help of the provided IAccountContext
360+
}
361+
}
362+
```
363+
364+
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`.
365+
366+
**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`.
367+
336368
### Built-In Feature Filters
337369

338370
There a few feature filters that come with the `Microsoft.FeatureManagement` package. These feature filters are not added automatically, but can be referenced and registered as soon as the package is registered.
@@ -360,7 +392,8 @@ This filter provides the capability to enable a feature based on a set percentag
360392

361393
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.
362394

363-
``` "EnhancedPipeline": {
395+
```
396+
"EnhancedPipeline": {
364397
"EnabledFor": [
365398
{
366399
"Name": "Microsoft.TimeWindow",

build.cmd

+3-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
powershell.exe -ExecutionPolicy Unrestricted -NoProfile -File "%~dp0build.ps1"
1+
call %~dp0build\InstallPowerShellCore.cmd
2+
3+
%PowerShellCore% "%~dp0build.ps1" %*

build.ps1

+14-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ Indicates whether the build config should be set to Debug or Release. The defaul
1010
param(
1111
[Parameter()]
1212
[ValidateSet('Debug','Release')]
13-
[string]$BuildConfig = "Release"
13+
[string]$BuildConfig = "Release",
14+
15+
[Parameter()]
16+
[switch]$RestoreOnly = $false
1417
)
1518

1619
$ErrorActionPreference = "Stop"
@@ -24,7 +27,15 @@ if ((Test-Path -Path $LogDirectory) -ne $true) {
2427
New-Item -ItemType Directory -Path $LogDirectory | Write-Verbose
2528
}
2629

27-
# Build
28-
dotnet build -o "$BuildRelativePath" -c $BuildConfig "$Solution" | Tee-Object -FilePath "$LogDirectory\build.log"
30+
if ($RestoreOnly)
31+
{
32+
# Restore
33+
dotnet restore "$Solution"
34+
}
35+
else
36+
{
37+
# Build
38+
dotnet build -o "$BuildRelativePath" -c $BuildConfig "$Solution" | Tee-Object -FilePath "$LogDirectory\build.log"
39+
}
2940

3041
exit $LASTEXITCODE

build/InstallPowerShellCore.cmd

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
if not exist "%USERPROFILE%\.dotnet\tools\pwsh.exe" (
2+
dotnet tool install --global PowerShell --version 6.2.3
3+
)
4+
5+
set PowerShellCore="%USERPROFILE%\.dotnet\tools\pwsh.exe"

build/NugetProperties.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<MinutesSinceMidnight>$([MSBuild]::Divide($(TicksSinceMidnight), 600000000))</MinutesSinceMidnight>
2020
<Floored>$([System.Math]::Floor($(MinutesSinceMidnight)))</Floored>
2121
<Revision>$(Floored)</Revision>
22-
<Version>1.0.0-preview-$(CDP_PATCH_NUMBER)-$(Revision)</Version>
22+
<Version>2.0.0-preview-$(CDP_PATCH_NUMBER)-$(Revision)</Version>
2323
</PropertyGroup>
2424

2525
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Consoto.Banking.AccountService.FeatureFilters;
5+
6+
namespace Consoto.Banking.AccountService
7+
{
8+
class AccountServiceContext : IAccountContext
9+
{
10+
public string AccountId { get; set; }
11+
}
12+
}

examples/ConsoleApp/ConsoleApp.csproj

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp2.1</TargetFramework>
6+
<RootNamespace>Consoto.Banking.AccountService</RootNamespace>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
11+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\..\src\Microsoft.FeatureManagement\Microsoft.FeatureManagement.csproj" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<None Update="appsettings.json">
20+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
21+
</None>
22+
</ItemGroup>
23+
24+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Consoto.Banking.AccountService.FeatureFilters;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.FeatureManagement;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Threading.Tasks;
10+
11+
namespace Consoto.Banking.AccountService.FeatureManagement
12+
{
13+
/// <summary>
14+
/// 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.
15+
/// This filter will only be executed if an object implementing <see cref="IAccountContext"/> is passed in during feature evaluation.
16+
/// </summary>
17+
[FilterAlias("AccountId")]
18+
class AccountIdFilter : IContextualFeatureFilter<IAccountContext>
19+
{
20+
public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureEvaluationContext, IAccountContext accountContext)
21+
{
22+
if (string.IsNullOrEmpty(accountContext?.AccountId))
23+
{
24+
throw new ArgumentNullException(nameof(accountContext));
25+
}
26+
27+
var allowedAccounts = new List<string>();
28+
29+
featureEvaluationContext.Parameters.Bind("AllowedAccounts", allowedAccounts);
30+
31+
return Task.FromResult(allowedAccounts.Contains(accountContext.AccountId));
32+
}
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
namespace Consoto.Banking.AccountService.FeatureFilters
5+
{
6+
public interface IAccountContext
7+
{
8+
string AccountId { get; }
9+
}
10+
}

examples/ConsoleApp/Program.cs

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
//
4+
using Consoto.Banking.AccountService.FeatureManagement;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.FeatureManagement;
8+
using Microsoft.FeatureManagement.FeatureFilters;
9+
using System;
10+
using System.Collections.Generic;
11+
using System.Threading.Tasks;
12+
13+
namespace Consoto.Banking.AccountService
14+
{
15+
class Program
16+
{
17+
public static async Task Main(string[] args)
18+
{
19+
//
20+
// Setup configuration
21+
IConfiguration configuration = new ConfigurationBuilder()
22+
.AddJsonFile("appsettings.json")
23+
.Build();
24+
25+
//
26+
// Setup application services + feature management
27+
IServiceCollection services = new ServiceCollection();
28+
29+
services.AddSingleton(configuration)
30+
.AddFeatureManagement()
31+
.AddFeatureFilter<PercentageFilter>()
32+
.AddFeatureFilter<AccountIdFilter>();
33+
34+
//
35+
// Get the feature manager from application services
36+
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
37+
{
38+
IFeatureManager featureManager = serviceProvider.GetRequiredService<IFeatureManager>();
39+
40+
var accounts = new List<string>()
41+
{
42+
"abc",
43+
"adef",
44+
"abcdefghijklmnopqrstuvwxyz"
45+
};
46+
47+
//
48+
// Mimic work items in a task-driven console application
49+
foreach (var account in accounts)
50+
{
51+
const string FeatureName = "Beta";
52+
53+
//
54+
// Check if feature enabled
55+
//
56+
var accountServiceContext = new AccountServiceContext
57+
{
58+
AccountId = account
59+
};
60+
61+
bool enabled = await featureManager.IsEnabledAsync(FeatureName, accountServiceContext);
62+
63+
//
64+
// Output results
65+
Console.WriteLine($"The {FeatureName} feature is {(enabled ? "enabled" : "disabled")} for the '{account}' account.");
66+
}
67+
}
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)