diff --git a/Microsoft.FeatureManagement.sln b/Microsoft.FeatureManagement.sln
index cbf51a01..ad4d8a7a 100644
--- a/Microsoft.FeatureManagement.sln
+++ b/Microsoft.FeatureManagement.sln
@@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "examples\Cons
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TargetingConsoleApp", "examples\TargetingConsoleApp\TargetingConsoleApp.csproj", "{6558C21E-CF20-4278-AA08-EB9D1DF29D66}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomAssignmentConsoleApp", "examples\CustomAssignmentConsoleApp\CustomAssignmentConsoleApp.csproj", "{06C10E31-4C33-4567-85DB-00056A2BB511}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -49,6 +51,10 @@ Global
{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
+ {06C10E31-4C33-4567-85DB-00056A2BB511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {06C10E31-4C33-4567-85DB-00056A2BB511}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {06C10E31-4C33-4567-85DB-00056A2BB511}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {06C10E31-4C33-4567-85DB-00056A2BB511}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -58,6 +64,7 @@ Global
{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}
+ {06C10E31-4C33-4567-85DB-00056A2BB511} = {FB5C34DF-695C-4DF9-8AED-B3EA2516EA72}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {84DA6C54-F140-4518-A1B4-E4CF42117FBD}
diff --git a/examples/CustomAssignmentConsoleApp/CustomAssignmentConsoleApp.csproj b/examples/CustomAssignmentConsoleApp/CustomAssignmentConsoleApp.csproj
new file mode 100644
index 00000000..68f2c659
--- /dev/null
+++ b/examples/CustomAssignmentConsoleApp/CustomAssignmentConsoleApp.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net5.0
+ Consoto.Banking.AccountService
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/examples/CustomAssignmentConsoleApp/DailyDiscountOptions.cs b/examples/CustomAssignmentConsoleApp/DailyDiscountOptions.cs
new file mode 100644
index 00000000..71964e6f
--- /dev/null
+++ b/examples/CustomAssignmentConsoleApp/DailyDiscountOptions.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+namespace Consoto.Banking.HelpDesk
+{
+ class DailyDiscountOptions
+ {
+ public string ProductName { get; set; }
+
+ public int Discount { get; set; }
+ }
+}
diff --git a/examples/CustomAssignmentConsoleApp/Program.cs b/examples/CustomAssignmentConsoleApp/Program.cs
new file mode 100644
index 00000000..c1f91629
--- /dev/null
+++ b/examples/CustomAssignmentConsoleApp/Program.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Consoto.Banking.AccountService;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.FeatureManagement;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Consoto.Banking.HelpDesk
+{
+ class Program
+ {
+ public static async Task Main(string[] args)
+ {
+ //
+ // Setup configuration
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json", false, true)
+ .Build();
+
+ //
+ // Setup application services + feature management
+ IServiceCollection services = new ServiceCollection();
+
+ services.AddSingleton(typeof(IFeatureVariantAssignerMetadata), typeof(RecurringAssigner));
+
+ services.AddSingleton(configuration)
+ .AddFeatureManagement()
+ .AddFeatureVariantAssigner();
+
+ //
+ // Get the feature manager from application services
+ using (ServiceProvider serviceProvider = services.BuildServiceProvider())
+ {
+ IFeatureVariantManager variantManager = serviceProvider.GetRequiredService();
+
+ DailyDiscountOptions discountOptions = await variantManager
+ .GetVariantAsync("DailyDiscount", CancellationToken.None);
+
+ //
+ // Output results
+ Console.WriteLine($"Today there is a {discountOptions.Discount}% discount on {discountOptions.ProductName}!");
+ }
+ }
+ }
+}
diff --git a/examples/CustomAssignmentConsoleApp/RecurringAssigner.cs b/examples/CustomAssignmentConsoleApp/RecurringAssigner.cs
new file mode 100644
index 00000000..47ded1fc
--- /dev/null
+++ b/examples/CustomAssignmentConsoleApp/RecurringAssigner.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.Extensions.Configuration;
+using Microsoft.FeatureManagement;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Consoto.Banking.AccountService
+{
+ [AssignerAlias("Recurring")]
+ class RecurringAssigner : IFeatureVariantAssigner
+ {
+ public ValueTask AssignVariantAsync(FeatureVariantAssignmentContext variantAssignmentContext, CancellationToken _)
+ {
+ FeatureDefinition featureDefinition = variantAssignmentContext.FeatureDefinition;
+
+ FeatureVariant chosenVariant = null;
+
+ string currentDay = DateTimeOffset.UtcNow.DayOfWeek.ToString();
+
+ foreach (var variant in featureDefinition.Variants)
+ {
+ RecurringAssignmentParameters p = variant.AssignmentParameters.Get() ??
+ new RecurringAssignmentParameters();
+
+ if (p.Days != null &&
+ p.Days.Any(d => d.Equals(currentDay, StringComparison.OrdinalIgnoreCase)))
+ {
+ chosenVariant = variant;
+
+ break;
+ }
+ }
+
+ return new ValueTask(chosenVariant);
+ }
+ }
+}
diff --git a/examples/CustomAssignmentConsoleApp/RecurringAssignmentParameters.cs b/examples/CustomAssignmentConsoleApp/RecurringAssignmentParameters.cs
new file mode 100644
index 00000000..34518abe
--- /dev/null
+++ b/examples/CustomAssignmentConsoleApp/RecurringAssignmentParameters.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using System.Collections.Generic;
+
+namespace Consoto.Banking.AccountService
+{
+ class RecurringAssignmentParameters
+ {
+ public List Days { get; set; }
+ }
+}
diff --git a/examples/CustomAssignmentConsoleApp/appsettings.json b/examples/CustomAssignmentConsoleApp/appsettings.json
new file mode 100644
index 00000000..8cbd7ee8
--- /dev/null
+++ b/examples/CustomAssignmentConsoleApp/appsettings.json
@@ -0,0 +1,33 @@
+{
+ "FeatureManagement": {
+ "DailyDiscount": {
+ "Assigner": "Recurring",
+ "Variants": [
+ {
+ "Default": true,
+ "Name": "Default",
+ "ConfigurationReference": "DailyDiscount:Default"
+ },
+ {
+ "Name": "Special",
+ "ConfigurationReference": "DailyDiscount:Special",
+ "AssignmentParameters": {
+ "Days": [
+ "Tuesday"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "DailyDiscount": {
+ "Default": {
+ "Discount": 20,
+ "ProductName": "Bananas"
+ },
+ "Special": {
+ "Discount": 30,
+ "ProductName": "Fish"
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/FeatureFlagDemo/Controllers/HomeController.cs b/examples/FeatureFlagDemo/Controllers/HomeController.cs
index a939a88e..e6f47169 100644
--- a/examples/FeatureFlagDemo/Controllers/HomeController.cs
+++ b/examples/FeatureFlagDemo/Controllers/HomeController.cs
@@ -31,9 +31,9 @@ public async Task About(CancellationToken cancellationToken)
{
ViewData["Message"] = "Your application description page.";
- if (await _featureManager.IsEnabledAsync(nameof(MyFeatureFlags.CustomViewData), cancellationToken))
+ if (await _featureManager.IsEnabledAsync(MyFeatureFlags.CustomViewData, cancellationToken))
{
- ViewData["Message"] = $"This is FANCY CONTENT you can see only if '{nameof(MyFeatureFlags.CustomViewData)}' is enabled.";
+ ViewData["Message"] = $"This is FANCY CONTENT you can see only if '{MyFeatureFlags.CustomViewData}' is enabled.";
};
return View();
diff --git a/examples/FeatureFlagDemo/DiscountBannerOptions.cs b/examples/FeatureFlagDemo/DiscountBannerOptions.cs
new file mode 100644
index 00000000..a458b5b2
--- /dev/null
+++ b/examples/FeatureFlagDemo/DiscountBannerOptions.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+namespace FeatureFlagDemo
+{
+ public class DiscountBannerOptions
+ {
+ public int Size { get; set; }
+
+ public string Color { get; set; }
+
+ public string Background { get; set; }
+ }
+}
diff --git a/examples/FeatureFlagDemo/MyFeatureFlags.cs b/examples/FeatureFlagDemo/MyFeatureFlags.cs
index 9441c531..3fc1be99 100644
--- a/examples/FeatureFlagDemo/MyFeatureFlags.cs
+++ b/examples/FeatureFlagDemo/MyFeatureFlags.cs
@@ -3,14 +3,12 @@
//
namespace FeatureFlagDemo
{
- //
- // Define feature flags in an enum
- public enum MyFeatureFlags
+ static class MyFeatureFlags
{
- Home,
- Beta,
- CustomViewData,
- ContentEnhancement,
- EnhancedPipeline
+ public const string Home = "Home";
+ public const string Beta = "Beta";
+ public const string CustomViewData = "CustomViewData";
+ public const string ContentEnhancement = "ContentEnhancement";
+ public const string EnhancedPipeline = "EnhancedPipeline";
}
}
diff --git a/examples/FeatureFlagDemo/Startup.cs b/examples/FeatureFlagDemo/Startup.cs
index 65c0777c..573d1fe3 100644
--- a/examples/FeatureFlagDemo/Startup.cs
+++ b/examples/FeatureFlagDemo/Startup.cs
@@ -59,6 +59,7 @@ public void ConfigureServices(IServiceCollection services)
.AddFeatureFilter()
.AddFeatureFilter()
.AddFeatureFilter()
+ .AddFeatureVariantAssigner()
.UseDisabledFeaturesHandler(new FeatureNotEnabledDisabledHandler());
services.AddMvc(o =>
diff --git a/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml b/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml
index 8ca281ef..516eb3ba 100644
--- a/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml
+++ b/examples/FeatureFlagDemo/Views/Shared/_Layout.cshtml
@@ -1,4 +1,10 @@
-
+@using Microsoft.FeatureManagement
+@inject IFeatureVariantManager variantManager;
+@{
+ DiscountBannerOptions opts = await variantManager.GetVariantAsync("DiscountBanner", Context.RequestAborted);
+}
+
+
@@ -15,6 +21,14 @@
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
+
-
+
+ New Sale, 50% Off !
+
@RenderBody()
diff --git a/examples/FeatureFlagDemo/appsettings.json b/examples/FeatureFlagDemo/appsettings.json
index d0a37270..919af4bf 100644
--- a/examples/FeatureFlagDemo/appsettings.json
+++ b/examples/FeatureFlagDemo/appsettings.json
@@ -66,6 +66,51 @@
}
}
]
+ },
+ "DiscountBanner": {
+ "Assigner": "Targeting",
+ "Variants": [
+ {
+ "Default": true,
+ "Name": "Big",
+ "ConfigurationReference": "DiscountBanner:Big"
+ },
+ {
+ "Name": "Small",
+ "ConfigurationReference": "DiscountBanner:Small",
+ "AssignmentParameters": {
+ "Audience": {
+ "Users": [
+ "Jeff",
+ "Alicia"
+ ],
+ "Groups": [
+ {
+ "Name": "Ring0",
+ "RolloutPercentage": 80
+ },
+ {
+ "Name": "Ring1",
+ "RolloutPercentage": 50
+ }
+ ],
+ "DefaultRolloutPercentage": 20
+ }
+ }
+ }
+ ]
+ }
+ },
+ "DiscountBanner": {
+ "Small": {
+ "Size": 24,
+ "Color": "#c9f568",
+ "Background": "#f35220"
+ },
+ "Big": {
+ "Size": 48,
+ "Color": "#007cb3",
+ "Background": "#ffbb02"
}
}
}
diff --git a/examples/TargetingConsoleApp/CartOptions.cs b/examples/TargetingConsoleApp/CartOptions.cs
new file mode 100644
index 00000000..eb202dbd
--- /dev/null
+++ b/examples/TargetingConsoleApp/CartOptions.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+namespace Consoto.Banking.HelpDesk
+{
+ class CartOptions
+ {
+ public int Size { get; set; }
+
+ public string Color { get; set; }
+ }
+}
diff --git a/examples/TargetingConsoleApp/Program.cs b/examples/TargetingConsoleApp/Program.cs
index 10e64223..464cb417 100644
--- a/examples/TargetingConsoleApp/Program.cs
+++ b/examples/TargetingConsoleApp/Program.cs
@@ -30,7 +30,8 @@ public static async Task Main(string[] args)
services.AddSingleton(configuration)
.AddFeatureManagement()
- .AddFeatureFilter();
+ .AddFeatureFilter()
+ .AddFeatureVariantAssigner();
IUserRepository userRepository = new InMemoryUserRepository();
@@ -39,6 +40,7 @@ public static async Task Main(string[] args)
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
IFeatureManager featureManager = serviceProvider.GetRequiredService();
+ IFeatureVariantManager variantManager = serviceProvider.GetRequiredService();
//
// We'll simulate a task to run on behalf of each known user
@@ -49,7 +51,8 @@ public static async Task Main(string[] args)
// Mimic work items in a task-driven console application
foreach (string userId in userIds)
{
- const string FeatureName = "Beta";
+ const string FeatureFlagName = "Beta";
+ const string DynamicFeatureName = "ShoppingCart";
//
// Get user
@@ -63,11 +66,27 @@ public static async Task Main(string[] args)
Groups = user.Groups
};
- bool enabled = await featureManager.IsEnabledAsync(FeatureName, targetingContext, CancellationToken.None);
+ //
+ // Evaluate feature flag using targeting
+ bool enabled = await featureManager
+ .IsEnabledAsync(
+ FeatureFlagName,
+ targetingContext,
+ CancellationToken.None);
+
+ //
+ // Retrieve feature variant using targeting
+ CartOptions cartOptions = await variantManager
+ .GetVariantAsync(
+ DynamicFeatureName,
+ targetingContext,
+ CancellationToken.None);
//
// Output results
- Console.WriteLine($"The {FeatureName} feature is {(enabled ? "enabled" : "disabled")} for the user '{userId}'.");
+ Console.WriteLine($"The {FeatureFlagName} feature is {(enabled ? "enabled" : "disabled")} for the user '{userId}'.");
+
+ Console.WriteLine($"User {user.Id} has a {cartOptions.Color} cart with a size of {cartOptions.Size} pixels.");
}
}
}
diff --git a/examples/TargetingConsoleApp/appsettings.json b/examples/TargetingConsoleApp/appsettings.json
index a5e827d6..a17902c7 100644
--- a/examples/TargetingConsoleApp/appsettings.json
+++ b/examples/TargetingConsoleApp/appsettings.json
@@ -24,6 +24,47 @@
}
}
]
+ },
+ "ShoppingCart": {
+ "Assigner": "Targeting",
+ "Variants": [
+ {
+ "Default": true,
+ "Name": "Big",
+ "ConfigurationReference": "ShoppingCart:Big",
+ "AssignmentParameters": {
+ "Audience": {
+ "Users": [
+ "Alec",
+ "Jeff",
+ "Alicia"
+ ]
+ }
+ }
+ },
+ {
+ "Name": "Small",
+ "ConfigurationReference": "ShoppingCart:Small",
+ "AssignmentParameters": {
+ "Audience": {
+ "Users": [
+ "Susan",
+ "JohnDoe"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ },
+ "ShoppingCart": {
+ "Big": {
+ "Size": 400,
+ "Color": "green"
+ },
+ "Small": {
+ "Size": 150,
+ "Color": "gray"
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.FeatureManagement/AssignerAliasAttribute.cs b/src/Microsoft.FeatureManagement/AssignerAliasAttribute.cs
new file mode 100644
index 00000000..f7d8f52b
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/AssignerAliasAttribute.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using System;
+
+namespace Microsoft.FeatureManagement
+{
+ ///
+ /// Allows the name of an to be customized to relate to the name specified in configuration.
+ ///
+ public class AssignerAliasAttribute : Attribute
+ {
+ ///
+ /// Creates an assigner alias using the provided alias.
+ ///
+ /// The alias of the feature variant assigner.
+ public AssignerAliasAttribute(string alias)
+ {
+ if (string.IsNullOrEmpty(alias))
+ {
+ throw new ArgumentNullException(nameof(alias));
+ }
+
+ Alias = alias;
+ }
+
+ ///
+ /// The name that will be used to match feature feature variant assigners specified in the configuration.
+ ///
+ public string Alias { get; }
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
index bac5a94a..2c8fe03d 100644
--- a/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
+++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureDefinitionProvider.cs
@@ -19,6 +19,7 @@ namespace Microsoft.FeatureManagement
sealed class ConfigurationFeatureDefinitionProvider : IFeatureDefinitionProvider, IDisposable
{
private const string FeatureFiltersSectionName = "EnabledFor";
+ private const string FeatureVariantsSectionName = "Variants";
private readonly IConfiguration _configuration;
private readonly ConcurrentDictionary _definitions;
private IDisposable _changeSubscription;
@@ -128,6 +129,10 @@ We support
var enabledFor = new List();
+ var variants = new List();
+
+ string assigner = null;
+
string val = configurationSection.Value; // configuration[$"{featureName}"];
if (string.IsNullOrEmpty(val))
@@ -159,19 +164,39 @@ We support
// Are accessed through the configuration system by using the array index as the property name, e.g. "myKey": { "0": "some", "1": "values" }
if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureFilterConfiguration.Name)]))
{
- enabledFor.Add(new FeatureFilterConfiguration()
+ enabledFor.Add(new FeatureFilterConfiguration
{
Name = section[nameof(FeatureFilterConfiguration.Name)],
Parameters = section.GetSection(nameof(FeatureFilterConfiguration.Parameters))
});
}
}
+
+ IEnumerable variantSections = configurationSection.GetSection(FeatureVariantsSectionName).GetChildren();
+
+ foreach (IConfigurationSection section in variantSections)
+ {
+ if (int.TryParse(section.Key, out int i) && !string.IsNullOrEmpty(section[nameof(FeatureVariant.Name)]))
+ {
+ variants.Add(new FeatureVariant
+ {
+ Default = section.GetValue(nameof(FeatureVariant.Default)),
+ Name = section.GetValue(nameof(FeatureVariant.Name)),
+ ConfigurationReference = section.GetValue(nameof(FeatureVariant.ConfigurationReference)),
+ AssignmentParameters = section.GetSection(nameof(FeatureVariant.AssignmentParameters))
+ });
+ }
+ }
+
+ assigner = configurationSection.GetValue(nameof(FeatureDefinition.Assigner));
}
return new FeatureDefinition()
{
Name = configurationSection.Key,
- EnabledFor = enabledFor
+ EnabledFor = enabledFor,
+ Variants = variants,
+ Assigner = assigner
};
}
diff --git a/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs
new file mode 100644
index 00000000..dcb32a0e
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/ConfigurationFeatureVariantOptionsResolver.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.Extensions.Configuration;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.FeatureManagement
+{
+ ///
+ /// A feature variant options resolver that resolves options by reading configuration from the .NET Core system.
+ ///
+ sealed class ConfigurationFeatureVariantOptionsResolver : IFeatureVariantOptionsResolver
+ {
+ private readonly IConfiguration _configuration;
+
+ public ConfigurationFeatureVariantOptionsResolver(IConfiguration configuration)
+ {
+ _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ }
+
+ public ValueTask GetOptionsAsync(FeatureDefinition featureDefinition, FeatureVariant variant, CancellationToken cancellationToken)
+ {
+ if (variant == null)
+ {
+ throw new ArgumentNullException(nameof(variant));
+ }
+
+ IConfiguration configuration = _configuration.GetSection($"{variant.ConfigurationReference}");
+
+ return new ValueTask(configuration.Get());
+ }
+ }
+}
diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs
index 53df9f9c..9f75edd9 100644
--- a/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs
+++ b/src/Microsoft.FeatureManagement/ContextualFeatureFilterEvaluator.cs
@@ -46,6 +46,11 @@ public ContextualFeatureFilterEvaluator(IFeatureFilterMetadata filter, Type appC
public Task EvaluateAsync(FeatureFilterEvaluationContext evaluationContext, object context, CancellationToken cancellationToken)
{
+ if (evaluationContext == null)
+ {
+ throw new ArgumentNullException(nameof(evaluationContext));
+ }
+
if (_evaluateFunc == null)
{
return Task.FromResult(false);
@@ -56,6 +61,16 @@ public Task EvaluateAsync(FeatureFilterEvaluationContext evaluationContext
public static bool IsContextualFilter(IFeatureFilterMetadata filter, Type appContextType)
{
+ if (filter == null)
+ {
+ throw new ArgumentNullException(nameof(filter));
+ }
+
+ if (appContextType == null)
+ {
+ throw new ArgumentNullException(nameof(appContextType));
+ }
+
return GetContextualFilterInterface(filter, appContextType) != null;
}
diff --git a/src/Microsoft.FeatureManagement/ContextualFeatureVariantAssignerEvaluator.cs b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAssignerEvaluator.cs
new file mode 100644
index 00000000..edfbd8de
--- /dev/null
+++ b/src/Microsoft.FeatureManagement/ContextualFeatureVariantAssignerEvaluator.cs
@@ -0,0 +1,127 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.FeatureManagement
+{
+ ///
+ /// Provides a performance efficient method of evaluating without knowing what the generic type parameter is.
+ ///
+ sealed class ContextualFeatureVariantAssignerEvaluator : IContextualFeatureVariantAssigner