Support contextual variant service provider and feature status fallback - #611
Conversation
There was a problem hiding this comment.
Pull request overview
This PR expands the variant-service injection capabilities in the Feature Management .NET SDK by introducing a contextual variant service provider API and adding an optional “feature enabled/disabled” fallback path when no variant-based service can be resolved.
Changes:
- Introduces
IContextualVariantServiceProvider<TService>and updates the internalVariantServiceProvider<TService>to support context-aware evaluation. - Adds
WithVariantService<TService, TEnabled, TDisabled>(featureName)to allow falling back to an enabled/disabled implementation when variant resolution fails. - Refactors and extends tests into a dedicated
VariantServiceProviderTestsuite, including new coverage for contextual behavior and feature-status fallback.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Tests.FeatureManagement/VariantServices.cs | Adds alias-based test implementations used by new variant/fallback tests. |
| tests/Tests.FeatureManagement/VariantServiceProviderTest.cs | New test suite covering variant DI, keyed service resolution, contextual provider behavior, and status fallback. |
| tests/Tests.FeatureManagement/FeatureManagementTest.cs | Removes variant service provider tests that were moved to the new dedicated test file. |
| tests/Tests.FeatureManagement/AppContext.cs | Adds an additional context type for tests (currently unused). |
| src/Microsoft.FeatureManagement/VariantServiceProvider.cs | Implements contextual service retrieval and optional feature-status fallback. |
| src/Microsoft.FeatureManagement/IContextualVariantServiceProvider.cs | New public interface for contextual variant service providers. |
| src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs | Adds new DI builder overload enabling feature-status fallback and registers contextual provider interface. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Hey Степан (@Stepami), do you have any concern about this PR? |
|
hey Zhiyuan Liang (@zhiyuanliang-ms)! i'll take a look today, been busy for the last few weeks |
| Type implementationType = enabled ? _fallbackWhenEnabled : _fallbackWhenDisabled; | ||
|
|
||
| if (implementationType != null) | ||
| { | ||
| return _variantServiceCache.GetOrAdd(GetVariantServiceName(implementationType), ResolveVariantService); | ||
| } | ||
|
|
||
| return null; |
There was a problem hiding this comment.
so if i want to obtain service based on status from keyed do i need to register it by string name?
services.AddSingleton<IService, EnabledService>(nameof(EnabledService));i would like to update docs in this PR
There was a problem hiding this comment.
so if i want to obtain service based on status from keyed do i need to register it by string name?
No.
My expected usage is:
services.AddSingleton<IService, EnabledService>();
services.AddSingleton<IService, DisabledService>();
services.AddFeatureManagement()
.WithVariantService<IService, EnabledService, DisabledService>("MyFeature");Comparing to keyed registration as below, the above code is more intuitive. But using keyed service registration does have the benefit of lazy instantiation.
services.AddKeyedSingleton<IService, EnabledService>(nameof(EnabledService));
services.AddKeyedSingleton<IService, DisabledService>(nameof(DisabledService));
services.AddFeatureManagement()
.WithVariantService<IService, EnabledService, DisabledService>("MyFeature");There was a problem hiding this comment.
Example app is also updated
| if (useContext) | ||
| { | ||
| enabled = await _featureManager.IsEnabledAsync(_featureName, context, cancellationToken); | ||
| } | ||
| else | ||
| { | ||
| enabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken); | ||
| } |
There was a problem hiding this comment.
do we really need this branching here?
await _featureManager.IsEnabledAsync(_featureName, cancellationToken)is basically
await _featureManager.IsEnabledAsync<object>(_featureName, null, cancellationToken)useContext is internal configuration, so a developer can't change it
There was a problem hiding this comment.
IsEnabledAsync<object>(_featureName, null, cancellationToken) will enter the code path useContext: true
And NRE will happen here
There was a problem hiding this comment.
But actually your question reminds me that there is gap between the IsEnabledAsync(featureName, context, cancellationToken) and GetVariantAsync(featureName, context, cancellationToken)
We will throw exception in GetVariantAsync when context is null. We should do the samething for IsEnabledAsync
Why this PR?
#604 #608
IVariantServiceProvider<TService>.GetServiceAsynccurrently does not accept a context, which limits it to scenarios that rely on ambient context.This creates a capability gap between
VariantServiceProviderandIVariantFeatureManager, because bothIVariantFeatureManager.IsEnabledAsyncandIVariantFeatureManager.GetVariantAsyncprovide overloads that accept an explicit context.How to fix
This PR introduces
IContextualVariantServiceProvider<TService>with methodGetServiceAsync<TContext>(TContext context, CancellationToken cancellationToken)The PR also adds
WithVariantService<TService, TEnabled, TDisabled>(featureName)which supports falling back to a service based on the feature’s enabled or disabled status.Example usage
Register the implementations used when the feature is enabled or disabled, and associate them with a feature flag:
Inject
IContextualVariantServiceProvider<TService>and supply the application context when resolving the service:If an assigned variant has a matching service registration, that implementation is returned. Otherwise,
EnabledServiceorDisabledServiceis selected according to the feature status evaluated with the supplied context.Keyed registrations are also supported and allow implementations to be instantiated lazily:
Service resolution behavior
Service resolution follows these steps:
GetVariantAsync.GetVariantAsynconly acceptsITargetingContext, so a non-targeting context is not used during variant resolution.TEnabled.TDisabled.Because
IsEnabledAsyncaccepts an arbitraryTContext, the feature-status fallback honors the supplied context even when it does not implementITargetingContext.