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