From 458b32278d550fc99892c9a02ef17e677d72b559 Mon Sep 17 00:00:00 2001
From: linglingye001 <143174321+linglingye001@users.noreply.github.com>
Date: Tue, 19 May 2026 15:43:15 +0800
Subject: [PATCH 1/4] Example app for paramters object (#598)
* Example app for paramters object
* update
---
.../InMemoryFeatureDefinitionProvider.cs | 76 +++++++++++++++++++
.../ParametersObjectConsoleApp.csproj | 13 ++++
.../ParametersObjectConsoleApp/Program.cs | 40 ++++++++++
examples/ParametersObjectConsoleApp/README.md | 33 ++++++++
4 files changed, 162 insertions(+)
create mode 100644 examples/ParametersObjectConsoleApp/InMemoryFeatureDefinitionProvider.cs
create mode 100644 examples/ParametersObjectConsoleApp/ParametersObjectConsoleApp.csproj
create mode 100644 examples/ParametersObjectConsoleApp/Program.cs
create mode 100644 examples/ParametersObjectConsoleApp/README.md
diff --git a/examples/ParametersObjectConsoleApp/InMemoryFeatureDefinitionProvider.cs b/examples/ParametersObjectConsoleApp/InMemoryFeatureDefinitionProvider.cs
new file mode 100644
index 00000000..702b7e84
--- /dev/null
+++ b/examples/ParametersObjectConsoleApp/InMemoryFeatureDefinitionProvider.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.FeatureManagement;
+using Microsoft.FeatureManagement.FeatureFilters;
+namespace ParametersObjectConsoleApp
+{
+ ///
+ /// A custom feature definition provider that supplies targeting filter settings
+ /// directly using the ParametersObject property, avoiding the need to construct IConfiguration.
+ ///
+ public class InMemoryFeatureDefinitionProvider : IFeatureDefinitionProvider
+ {
+ private readonly Dictionary _featureDefinitions;
+
+ public InMemoryFeatureDefinitionProvider()
+ {
+ //
+ // Define feature flags with targeting settings.
+ // This demonstrates supplying filter parameters directly via ParametersObject
+ // instead of constructing an IConfiguration with key-value pairs.
+ _featureDefinitions = new Dictionary
+ {
+ ["Beta"] = new FeatureDefinition
+ {
+ Name = "Beta",
+ EnabledFor = new List
+ {
+ new FeatureFilterConfiguration
+ {
+ Name = "Microsoft.Targeting",
+ ParametersObject = new TargetingFilterSettings
+ {
+ Audience = new Audience
+ {
+ Users = new List { "Jeff", "Anne" },
+ Groups = new List
+ {
+ new GroupRollout
+ {
+ Name = "Management",
+ RolloutPercentage = 100
+ },
+ new GroupRollout
+ {
+ Name = "TeamMembers",
+ RolloutPercentage = 45
+ }
+ },
+ DefaultRolloutPercentage = 20
+ }
+ }
+ }
+ }
+ }
+ };
+ }
+
+ public Task GetFeatureDefinitionAsync(string featureName)
+ {
+ _featureDefinitions.TryGetValue(featureName, out FeatureDefinition definition);
+
+ return Task.FromResult(definition);
+ }
+
+ public async IAsyncEnumerable GetAllFeatureDefinitionsAsync()
+ {
+ foreach (var definition in _featureDefinitions.Values)
+ {
+ yield return definition;
+ }
+
+ await Task.CompletedTask;
+ }
+ }
+}
diff --git a/examples/ParametersObjectConsoleApp/ParametersObjectConsoleApp.csproj b/examples/ParametersObjectConsoleApp/ParametersObjectConsoleApp.csproj
new file mode 100644
index 00000000..7d1a899b
--- /dev/null
+++ b/examples/ParametersObjectConsoleApp/ParametersObjectConsoleApp.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net8.0
+ enable
+
+
+
+
+
+
+
diff --git a/examples/ParametersObjectConsoleApp/Program.cs b/examples/ParametersObjectConsoleApp/Program.cs
new file mode 100644
index 00000000..c80a36bf
--- /dev/null
+++ b/examples/ParametersObjectConsoleApp/Program.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+//
+using Microsoft.FeatureManagement;
+using Microsoft.FeatureManagement.FeatureFilters;
+using ParametersObjectConsoleApp;
+
+//
+// Create a feature manager using a custom definition provider
+// that supplies targeting filter settings directly via ParametersObject.
+var featureManager = new FeatureManager(new InMemoryFeatureDefinitionProvider())
+{
+ FeatureFilters = new List { new ContextualTargetingFilter() }
+};
+
+//
+// Simulate evaluating the "Beta" feature for different users
+var users = new List<(string UserId, List Groups)>
+{
+ ("Jeff", new List { "TeamMembers" }), // Targeted by user name
+ ("Anne", new List { "Management" }), // Targeted by user name
+ ("Sam", new List { "Management" }), // Targeted by group (100% rollout)
+ ("Alice", new List { "TeamMembers" }), // May be targeted by group (45% rollout)
+ ("Bob", new List { "External" }) // Only targeted by default rollout (20%)
+};
+
+foreach (var (userId, groups) in users)
+{
+ const string FeatureName = "Beta";
+
+ var targetingContext = new TargetingContext
+ {
+ UserId = userId,
+ Groups = groups
+ };
+
+ bool enabled = await featureManager.IsEnabledAsync(FeatureName, targetingContext);
+
+ Console.WriteLine($"The {FeatureName} feature is {(enabled ? "enabled" : "disabled")} for the user '{userId}'.");
+}
diff --git a/examples/ParametersObjectConsoleApp/README.md b/examples/ParametersObjectConsoleApp/README.md
new file mode 100644
index 00000000..81845aac
--- /dev/null
+++ b/examples/ParametersObjectConsoleApp/README.md
@@ -0,0 +1,33 @@
+# ParametersObject Console App
+
+This example demonstrates how to use the `ParametersObject` property on `FeatureFilterConfiguration` to supply filter settings directly from a custom `IFeatureDefinitionProvider`.
+
+## Overview
+
+When implementing a custom `IFeatureDefinitionProvider` that sources feature definitions from alternative backends (e.g., databases, REST APIs), you no longer need to construct an `IConfiguration` object to pass filter parameters. Instead, you can assign settings like `TargetingFilterSettings` directly to `ParametersObject`.
+
+## Key Concept
+
+```csharp
+new FeatureFilterConfiguration
+{
+ Name = "Microsoft.Targeting",
+ ParametersObject = new TargetingFilterSettings
+ {
+ Audience = new Audience
+ {
+ Users = new List { "Jeff", "Anne" },
+ Groups = new List { ... },
+ DefaultRolloutPercentage = 20
+ }
+ }
+}
+```
+
+This eliminates the need for verbose `IConfiguration` construction with magic string keys.
+
+## Running
+
+```
+dotnet run --project examples/ParametersObjectConsoleApp/ParametersObjectConsoleApp.csproj
+```
From 0e8d5aa87fd960368bde6dfce70294d850d4504c Mon Sep 17 00:00:00 2001
From: linglingye001 <143174321+linglingye001@users.noreply.github.com>
Date: Tue, 19 May 2026 15:44:12 +0800
Subject: [PATCH 2/4] add release notes link in readme (#603)
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index b1dee8ab..be02d95b 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,8 @@ Feature management provides a way to develop and expose application functionalit
[**API reference**](https://go.microsoft.com/fwlink/?linkid=2091700): This API reference details the API surface of the libraries contained within this repository.
+[**Release Notes**](https://github.com/Azure/AppConfiguration/blob/main/releaseNotes/Microsoft.Featuremanagement.md): The release notes for all versions of the feature management libraries can be found here.
+
## Examples
* [.NET Console App](./examples/ConsoleApp)
From 02f4c2d5381e3ed4b5bc8b7b8fc461e576e0c31d Mon Sep 17 00:00:00 2001
From: Zhiyuan Liang <141655842+zhiyuanliang-ms@users.noreply.github.com>
Date: Tue, 2 Jun 2026 14:38:43 +0800
Subject: [PATCH 3/4] Support keyed service for variant service provider (#606)
* support keyed service
* support variant service provider options
* update
* update
* revert variant service provider options
* add comment
---
examples/VariantServiceDemo/Program.cs | 7 +-
.../FeatureManagementBuilderExtensions.cs | 4 +-
.../VariantServiceProvider.cs | 40 +++--
.../FeatureManagementTest.cs | 155 +++++++++++++++++-
4 files changed, 189 insertions(+), 17 deletions(-)
diff --git a/examples/VariantServiceDemo/Program.cs b/examples/VariantServiceDemo/Program.cs
index a866d02d..de43ffb9 100644
--- a/examples/VariantServiceDemo/Program.cs
+++ b/examples/VariantServiceDemo/Program.cs
@@ -19,10 +19,11 @@
builder.Services.AddApplicationInsightsTelemetry();
//
-// Add variant implementations of ICalculator
-builder.Services.AddSingleton();
+// Add variant implementations of ICalculator using keyed services so that only the
+// implementation matching the assigned variant is instantiated on demand.
+builder.Services.AddKeyedSingleton("DefaultCalculator");
-builder.Services.AddSingleton();
+builder.Services.AddKeyedSingleton("RemoteCalculator");
//
// Enter feature management
diff --git a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs
index f8635a79..53dd19cf 100644
--- a/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs
+++ b/src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs
@@ -63,14 +63,14 @@ public static IFeatureManagementBuilder WithVariantService(this IFeatu
builder.Services.AddScoped>(sp => new VariantServiceProvider(
featureName,
sp.GetRequiredService(),
- sp.GetRequiredService>()));
+ sp));
}
else
{
builder.Services.AddSingleton>(sp => new VariantServiceProvider(
featureName,
sp.GetRequiredService(),
- sp.GetRequiredService>()));
+ sp));
}
return builder;
diff --git a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs
index d4b3f514..16293fb1 100644
--- a/src/Microsoft.FeatureManagement/VariantServiceProvider.cs
+++ b/src/Microsoft.FeatureManagement/VariantServiceProvider.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
+using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -16,7 +17,7 @@ namespace Microsoft.FeatureManagement
///
internal class VariantServiceProvider : IVariantServiceProvider where TService : class
{
- private readonly IEnumerable _services;
+ private readonly IServiceProvider _serviceProvider;
private readonly IVariantFeatureManager _featureManager;
private readonly string _featureName;
private readonly ConcurrentDictionary _variantServiceCache;
@@ -26,15 +27,15 @@ internal class VariantServiceProvider : IVariantServiceProvider
/// The feature flag that should be used to determine which variant of the service should be used.
/// The feature manager to get the assigned variant of the feature flag.
- /// Implementation variants of TService.
+ /// The service provider used to resolve implementation variants of TService. If it implements , keyed resolution is used to enable lazy instantiation; otherwise all registered implementations are enumerated.
/// Thrown if is null.
/// Thrown if is null.
- /// Thrown if is null.
- public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IEnumerable services)
+ /// Thrown if is null.
+ public VariantServiceProvider(string featureName, IVariantFeatureManager featureManager, IServiceProvider serviceProvider)
{
_featureName = featureName ?? throw new ArgumentNullException(nameof(featureName));
_featureManager = featureManager ?? throw new ArgumentNullException(nameof(featureManager));
- _services = services ?? throw new ArgumentNullException(nameof(services));
+ _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_variantServiceCache = new ConcurrentDictionary();
}
@@ -55,16 +56,35 @@ public async ValueTask GetServiceAsync(CancellationToken cancellationT
{
implementation = _variantServiceCache.GetOrAdd(
variant.Name,
- (_) => _services.FirstOrDefault(
- service => IsMatchingVariantName(
- service.GetType(),
- variant.Name))
- );
+ (variantName) => ResolveVariantService(variantName));
}
return implementation;
}
+ private TService ResolveVariantService(string variantName)
+ {
+ //
+ // If the service provider supports keyed services, try to resolve the variant by its name as the key first.
+ // This allows lazy instantiation of the variant service.
+ if (_serviceProvider is IKeyedServiceProvider)
+ {
+ TService keyedService = _serviceProvider.GetKeyedService(variantName);
+
+ if (keyedService != null)
+ {
+ return keyedService;
+ }
+ }
+
+ //
+ // Fall back to enumerating all non-keyed registrations of TService and matching by VariantServiceAliasAttribute or the implementation type name.
+ IEnumerable services = _serviceProvider.GetRequiredService>();
+
+ return services.FirstOrDefault(
+ service => IsMatchingVariantName(service.GetType(), variantName));
+ }
+
private bool IsMatchingVariantName(Type implementationType, string variantName)
{
string implementationName = ((VariantServiceAliasAttribute)Attribute.GetCustomAttribute(implementationType, typeof(VariantServiceAliasAttribute)))?.Alias;
diff --git a/tests/Tests.FeatureManagement/FeatureManagementTest.cs b/tests/Tests.FeatureManagement/FeatureManagementTest.cs
index a70e6a0d..4219a518 100644
--- a/tests/Tests.FeatureManagement/FeatureManagementTest.cs
+++ b/tests/Tests.FeatureManagement/FeatureManagementTest.cs
@@ -524,12 +524,12 @@ public async Task MergesFeatureFlagsFromDifferentConfigurationSources()
* Feature1: true
* Feature2: true
* FeatureA: true
- *
+ *
* appsettings2.json
* Feature1: true
* Feature2: false
* FeatureB: true
- *
+ *
* appsettings3.json
* Feature1: false
* Feature2: false
@@ -2234,6 +2234,157 @@ public async Task VariantBasedInjection()
);
}
+ [Fact]
+ public async Task VariantServiceProviderResolvesKeyedService()
+ {
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+ IServiceCollection services = new ServiceCollection();
+
+ services.AddKeyedSingleton("AlgorithmBeta");
+ services.AddKeyedSingleton("Sigma");
+ services.AddKeyedSingleton("Omega", (sp, _) => new AlgorithmOmega("OMEGA"));
+
+ services.AddSingleton(configuration)
+ .AddFeatureManagement()
+ .AddFeatureFilter()
+ .WithVariantService(Features.VariantImplementationFeature);
+
+ var targetingContextAccessor = new OnDemandTargetingContextAccessor();
+
+ services.AddSingleton(targetingContextAccessor);
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>();
+
+ targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" };
+ IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);
+ Assert.NotNull(algorithm);
+ Assert.Equal("Beta", algorithm.Style);
+
+ targetingContextAccessor.Current = new TargetingContext { UserId = "UserSigma" };
+ algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);
+ Assert.NotNull(algorithm);
+ Assert.Equal("Sigma", algorithm.Style);
+
+ targetingContextAccessor.Current = new TargetingContext { UserId = "UserOmega" };
+ algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);
+ Assert.NotNull(algorithm);
+ Assert.Equal("OMEGA", algorithm.Style);
+ }
+
+ [Fact]
+ public async Task VariantServiceProviderKeyedServiceIsLazilyInstantiated()
+ {
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+ IServiceCollection services = new ServiceCollection();
+
+ int betaInstantiationCount = 0;
+ int sigmaInstantiationCount = 0;
+ int omegaInstantiationCount = 0;
+
+ services.AddKeyedSingleton("AlgorithmBeta", (sp, _) =>
+ {
+ betaInstantiationCount++;
+ return new AlgorithmBeta();
+ });
+ services.AddKeyedSingleton("Sigma", (sp, _) =>
+ {
+ sigmaInstantiationCount++;
+ return new AlgorithmSigma();
+ });
+ services.AddKeyedSingleton("Omega", (sp, _) =>
+ {
+ omegaInstantiationCount++;
+ return new AlgorithmOmega("OMEGA");
+ });
+
+ services.AddSingleton(configuration)
+ .AddFeatureManagement()
+ .AddFeatureFilter()
+ .WithVariantService(Features.VariantImplementationFeature);
+
+ var targetingContextAccessor = new OnDemandTargetingContextAccessor();
+
+ services.AddSingleton(targetingContextAccessor);
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>();
+
+ //
+ // No variant resolved yet - nothing should be instantiated.
+ Assert.Equal(0, betaInstantiationCount);
+ Assert.Equal(0, sigmaInstantiationCount);
+ Assert.Equal(0, omegaInstantiationCount);
+
+ //
+ // Resolve the Beta variant. Only AlgorithmBeta should be instantiated.
+ targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" };
+ IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);
+ Assert.Equal("Beta", algorithm.Style);
+ Assert.Equal(1, betaInstantiationCount);
+ Assert.Equal(0, sigmaInstantiationCount);
+ Assert.Equal(0, omegaInstantiationCount);
+
+ //
+ // Resolving Beta again should reuse the cached instance - no new instantiation.
+ algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);
+ Assert.Equal("Beta", algorithm.Style);
+ Assert.Equal(1, betaInstantiationCount);
+ Assert.Equal(0, sigmaInstantiationCount);
+ Assert.Equal(0, omegaInstantiationCount);
+
+ //
+ // Resolve the Sigma variant. Only AlgorithmSigma should be instantiated additionally.
+ targetingContextAccessor.Current = new TargetingContext { UserId = "UserSigma" };
+ algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);
+ Assert.Equal("Sigma", algorithm.Style);
+ Assert.Equal(1, betaInstantiationCount);
+ Assert.Equal(1, sigmaInstantiationCount);
+ Assert.Equal(0, omegaInstantiationCount);
+ }
+
+ [Fact]
+ public async Task VariantServiceProviderPrefersKeyedOverNonKeyed()
+ {
+ IConfiguration configuration = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+ IServiceCollection services = new ServiceCollection();
+
+ //
+ // Register both keyed and non-keyed implementations matching the same variant name.
+ // The keyed registration should take precedence.
+ services.AddSingleton();
+ services.AddKeyedSingleton("AlgorithmBeta", (sp, _) => new AlgorithmOmega("KeyedBeta"));
+
+ services.AddSingleton(configuration)
+ .AddFeatureManagement()
+ .AddFeatureFilter()
+ .WithVariantService(Features.VariantImplementationFeature);
+
+ var targetingContextAccessor = new OnDemandTargetingContextAccessor();
+
+ services.AddSingleton(targetingContextAccessor);
+
+ ServiceProvider serviceProvider = services.BuildServiceProvider();
+
+ IVariantServiceProvider featuredAlgorithm = serviceProvider.GetRequiredService>();
+
+ targetingContextAccessor.Current = new TargetingContext { UserId = "UserBeta" };
+ IAlgorithm algorithm = await featuredAlgorithm.GetServiceAsync(CancellationToken.None);
+ Assert.NotNull(algorithm);
+ Assert.Equal("KeyedBeta", algorithm.Style);
+ }
+
[Fact]
public async Task VariantFeatureFlagWithContextualFeatureFilter()
{
From 645002a191c62ff87f1dcd3e64c0f436b59b3b93 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Fri, 26 Jun 2026 12:06:12 +0800
Subject: [PATCH 4/4] Version bump 4.6.0 (#609)
* Version bump 4.6.0
---
.../Microsoft.FeatureManagement.AspNetCore.csproj | 2 +-
...osoft.FeatureManagement.Telemetry.ApplicationInsights.csproj | 2 +-
.../Microsoft.FeatureManagement.csproj | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
index 09861894..1b29c061 100644
--- a/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
+++ b/src/Microsoft.FeatureManagement.AspNetCore/Microsoft.FeatureManagement.AspNetCore.csproj
@@ -5,7 +5,7 @@
4
- 5
+ 6
0
diff --git a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
index bc7dfd1f..d4e35382 100644
--- a/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
+++ b/src/Microsoft.FeatureManagement.Telemetry.ApplicationInsights/Microsoft.FeatureManagement.Telemetry.ApplicationInsights.csproj
@@ -4,7 +4,7 @@
4
- 5
+ 6
0
diff --git a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
index 170eafa8..9f690d8e 100644
--- a/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
+++ b/src/Microsoft.FeatureManagement/Microsoft.FeatureManagement.csproj
@@ -5,7 +5,7 @@
4
- 5
+ 6
0