Skip to content

Support contextual variant service provider and feature status fallback - #611

Merged
Zhiyuan Liang (zhiyuanliang-ms) merged 2 commits into
mainfrom
zhiyuanliang/contextual-vsp
Aug 18, 2026
Merged

Support contextual variant service provider and feature status fallback#611
Zhiyuan Liang (zhiyuanliang-ms) merged 2 commits into
mainfrom
zhiyuanliang/contextual-vsp

Conversation

@zhiyuanliang-ms

@zhiyuanliang-ms Zhiyuan Liang (zhiyuanliang-ms) commented Jul 29, 2026

Copy link
Copy Markdown
Member

Why this PR?

#604 #608

IVariantServiceProvider<TService>.GetServiceAsync currently does not accept a context, which limits it to scenarios that rely on ambient context.

This creates a capability gap between VariantServiceProvider and IVariantFeatureManager, because both IVariantFeatureManager.IsEnabledAsync and IVariantFeatureManager.GetVariantAsync provide overloads that accept an explicit context.

How to fix

This PR introduces IContextualVariantServiceProvider<TService> with method GetServiceAsync<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:

services.AddSingleton<IMyService, EnabledService>();
services.AddSingleton<IMyService, DisabledService>();

services.AddFeatureManagement()
    .AddFeatureFilter<MyContextualFilter>()
    .WithVariantService<IMyService, EnabledService, DisabledService>(
        "MyFeature");

Inject IContextualVariantServiceProvider<TService> and supply the application context when resolving the service:

public sealed class RequestHandler
{
    private readonly IContextualVariantServiceProvider<IMyService> _serviceProvider;

    public RequestHandler(
        IContextualVariantServiceProvider<IMyService> serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task HandleAsync(
        AccountContext context,
        CancellationToken cancellationToken)
    {
        IMyService service = await _serviceProvider.GetServiceAsync(
            context,
            cancellationToken);

        await service.ExecuteAsync(cancellationToken);
    }
}

If an assigned variant has a matching service registration, that implementation is returned. Otherwise, EnabledService or DisabledService is selected according to the feature status evaluated with the supplied context.

Keyed registrations are also supported and allow implementations to be instantiated lazily:

services.AddKeyedSingleton<IMyService, EnabledService>(nameof(EnabledService));

services.AddKeyedSingleton<IMyService, DisabledService>(nameof(DisabledService));

Service resolution behavior

Service resolution follows these steps:

  1. Resolve the assigned variant.
  • When the supplied context implements ITargetingContext, pass it to GetVariantAsync.
  • When the supplied context does not implement ITargetingContext, skip variant resolution and use the context only when evaluating the feature-status fallback.
  • GetVariantAsync only accepts ITargetingContext, so a non-targeting context is not used during variant resolution.
  1. If a variant is assigned and a matching service is registered, return that service.
  2. Otherwise, fall back to the feature status.
  • If the feature is enabled, return TEnabled.
  • If the feature is disabled, return TDisabled.

Because IsEnabledAsync accepts an arbitrary TContext, the feature-status fallback honors the supplied context even when it does not implement ITargetingContext.

Copilot AI review requested due to automatic review settings July 29, 2026 09:46
Comment thread tests/Tests.FeatureManagement/VariantServiceProviderTest.cs Fixed
Comment thread src/Microsoft.FeatureManagement/VariantServiceProvider.cs Dismissed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 internal VariantServiceProvider<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 VariantServiceProviderTest suite, 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.

Comment thread src/Microsoft.FeatureManagement/VariantServiceProvider.cs
Comment thread src/Microsoft.FeatureManagement/FeatureManagementBuilderExtensions.cs Outdated
Comment thread src/Microsoft.FeatureManagement/IContextualVariantServiceProvider.cs Outdated
Comment thread tests/Tests.FeatureManagement/AppContext.cs Outdated
@zhiyuanliang-ms

Copy link
Copy Markdown
Member Author

Hey Степан (@Stepami), do you have any concern about this PR?

@Stepami

Copy link
Copy Markdown

hey Zhiyuan Liang (@zhiyuanliang-ms)! i'll take a look today, been busy for the last few weeks

Comment on lines +119 to +126
Type implementationType = enabled ? _fallbackWhenEnabled : _fallbackWhenDisabled;

if (implementationType != null)
{
return _variantServiceCache.GetOrAdd(GetVariantServiceName(implementationType), ResolveVariantService);
}

return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example app is also updated

Comment on lines +110 to 117
if (useContext)
{
enabled = await _featureManager.IsEnabledAsync(_featureName, context, cancellationToken);
}
else
{
enabled = await _featureManager.IsEnabledAsync(_featureName, cancellationToken);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsEnabledAsync<object>(_featureName, null, cancellationToken) will enter the code path useContext: true

And NRE will happen here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@zhiyuanliang-ms
Zhiyuan Liang (zhiyuanliang-ms) merged commit ad812e2 into main Aug 18, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants