Skip to content

feat: Add multi-provider support #488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 60 commits into
base: main
Choose a base branch
from

Conversation

askpt
Copy link
Member

@askpt askpt commented Jun 6, 2025

This PR

This pull request introduces the experimental "Multi-Provider" feature to the OpenFeature library, enabling simultaneous use of multiple feature flag providers with configurable evaluation strategies. The changes include updates to documentation, new classes and methods to support multi-provider functionality, and a sample implementation in AspNetCore. Below is a summary of the most important changes grouped by theme.

Documentation Updates:

  • Added "Multi-Provider" to the feature list in README.md, marking it as experimental.
  • Included a detailed section in README.md explaining the usage, evaluation strategies, modes, and limitations of the Multi-Provider feature.

Sample Implementation:

  • Updated samples/AspNetCore/Program.cs to demonstrate Multi-Provider usage, including flag evaluation and error handling. [1] [2]

Core Multi-Provider Functionality:

  • Introduced the MultiProvider class in src/OpenFeature/Providers/MultiProvider/MultiProvider.cs, implementing support for multiple providers, evaluation strategies, initialization, and shutdown logic.
  • Added the ProviderEntry class to represent individual providers in the Multi-Provider configuration.

Supporting Models and Extensions:

  • Created ProviderStatus and RegisteredProvider models to track provider states and metadata. [1] [2]
  • Added ProviderExtensions to handle evaluation logic for individual providers.

Related Issues

Fixes #487

Notes

I didn't do proper testing for Hooks and Events. This PR is already quite big, and I would appreciate some feedback on the overall design.

Follow-up Tasks

  • Add Hooks and Events support for Multi-Provider.
  • Add Dependency Injection support.

How to test

Open the samples app and enjoy!

askpt added 13 commits June 5, 2025 16:42
…provider feature flag evaluation

Signed-off-by: André Silva <[email protected]>
…Strategy classes for feature flag evaluation

Signed-off-by: André Silva <[email protected]>
…pecific feature resolution

Signed-off-by: André Silva <[email protected]>
…ulti-type feature resolution

Signed-off-by: André Silva <[email protected]>
…hod for improved multi-provider support

Signed-off-by: André Silva <[email protected]>
Signed-off-by: André Silva <[email protected]>
…gy to enhance multi-provider support

Signed-off-by: André Silva <[email protected]>
…date multi-provider functionality

Signed-off-by: André Silva <[email protected]>
@askpt askpt linked an issue Jun 6, 2025 that may be closed by this pull request
Copy link

codecov bot commented Jun 6, 2025

Codecov Report

❌ Patch coverage is 95.70707% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.60%. Comparing base (2547a57) to head (6685f01).

Files with missing lines Patch % Lines
...enFeature/Providers/MultiProvider/MultiProvider.cs 95.67% 5 Missing and 3 partials ⚠️
...MultiProvider/Strategies/BaseEvaluationStrategy.cs 85.36% 2 Missing and 4 partials ⚠️
...ture/Providers/MultiProvider/ProviderExtensions.cs 92.00% 0 Missing and 2 partials ⚠️
...er/Strategies/Models/StrategyPerProviderContext.cs 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #488      +/-   ##
==========================================
+ Coverage   88.45%   89.60%   +1.15%     
==========================================
  Files          50       64      +14     
  Lines        2096     2492     +396     
  Branches      245      295      +50     
==========================================
+ Hits         1854     2233     +379     
- Misses        191      199       +8     
- Partials       51       60       +9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@toddbaert toddbaert self-requested a review June 19, 2025 12:57
@sergheevxo
Copy link

Do you have any idea when this will be implemented in the NuGet package?

@askpt
Copy link
Member Author

askpt commented Jun 25, 2025

Do you have any idea when this will be implemented in the NuGet package?

@sergheevxo I am actively working on #338, and this feature is next on my list.

@askpt askpt requested a review from kylejuliandev July 7, 2025 18:58
github-merge-queue bot pushed a commit that referenced this pull request Jul 14, 2025
Signed-off-by: André Silva <[email protected]>

<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR
<!-- add the description of the PR here -->

This pull request introduces enhancements to the `Value` class in the
`OpenFeature.Model` namespace, ensuring better equality handling, and
updates dependencies to include `Microsoft.Bcl.HashCode`. The most
significant changes include implementing equality comparison for
`Value`, adding hash code generation, and updating project files to
include the new dependency.

### Enhancements to `Value` class:

*
[`src/OpenFeature/Model/Value.cs`](diffhunk://#diff-336aacd3c42458899187108a2064648ae21e439b2b11e6ca7f25b7b7fef00609L9-R9):
The `Value` class now implements `IEquatable<Value>` and includes
methods for equality comparison (`Equals`, `==`, `!=`), hash code
generation (`GetHashCode`), and internal helpers for comparing complex
types like structures and lists. This ensures more robust and consistent
equality checks.
[[1]](diffhunk://#diff-336aacd3c42458899187108a2064648ae21e439b2b11e6ca7f25b7b7fef00609L9-R9)
[[2]](diffhunk://#diff-336aacd3c42458899187108a2064648ae21e439b2b11e6ca7f25b7b7fef00609R187-R378)

### Dependency updates:

*
[`Directory.Packages.props`](diffhunk://#diff-5baf5f9e448ad54ab25a091adee0da05d4d228481c9200518fcb1b53a65d4156L13-R25):
Added `Microsoft.Bcl.HashCode` as a dependency for hash code generation.
Other package references were reformatted for consistency.
*
[`src/OpenFeature/OpenFeature.csproj`](diffhunk://#diff-711ea17cbdebe419375c7684c8c39a1423d2bebcf8976ddd7bdd78deaab65b21R11):
Included `Microsoft.Bcl.HashCode` for specific target frameworks
(`net462` and `netstandard2.0`).

### Notes
<!-- any additional notes for this PR -->
This implementation is necessary for the comparison in the
MultiProvider. See:
#488 (comment)

---------

Signed-off-by: André Silva <[email protected]>
Copy link
Contributor

@arttonoyan arttonoyan left a comment

Choose a reason for hiding this comment

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

Apologies for the premature approval - I need to take a closer look at the changes before this can move forward. I’ll complete my review in the next couple of days

try
{
await rp.Provider.InitializeAsync(context, cancellationToken).ConfigureAwait(false);
rp.SetStatus(Constant.ProviderStatus.Ready);
Copy link

Choose a reason for hiding this comment

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

Is this thread safe?

Copy link
Contributor

@arttonoyan arttonoyan Jul 27, 2025

Choose a reason for hiding this comment

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

InitializeAsync and ShutdownAsync iterate through _registeredProviders and update each provider’s status concurrently. Each RegisteredProvider instance’s Status property is mutated inside Task.WhenAll without synchronization

Updating shared mutable state in parallel can lead to data races if the Status property is not thread‑safe. Either update statuses sequentially, protect them with synchronization (e.g., locks) or redesign RegisteredProvider to be immutable and return a new instance with the updated status.


To ensure we don’t run into issues here, I would suggest consider adding an integration test that validates the concurrent status updates behave correctly. This will help verify thread-safety and prevent potential data races.

Copy link
Contributor

Choose a reason for hiding this comment

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

@askpt this was interesting to me so I played around with it a bit.
I’m sharing what I wrote so we don’t lose the code - feel free to improve it and reuse if it’s helpful!

Also, just an idea: it might be useful to add an internal GetRegisteredProviders() method in MultiProvider to make testing easier. Maybe even a public one if it makes sense longer term.

Here’s the test code I came up with:

[Fact]
public async Task MultiProvider_ConcurrentInitializationAndShutdown_ShouldMaintainConsistentProviderStatus()
{
    // Arrange
    const int providerCount = 20;
    var random = new Random();
    var providerEntries = new List<ProviderEntry>();

    for (int i = 0; i < providerCount; i++)
    {
        var provider = Substitute.For<FeatureProvider>();

        provider.InitializeAsync(Arg.Any<EvaluationContext>(), Arg.Any<CancellationToken>())
                .Returns(Task.CompletedTask);

        provider.ShutdownAsync(Arg.Any<CancellationToken>())
                .Returns(Task.CompletedTask);

        provider.GetMetadata()
                .Returns(new Metadata(name: $"provider-{i}"));

        providerEntries.Add(new ProviderEntry(provider));
    }

    var multiProvider = new MultiProvider(providerEntries);

    // Act: simulate concurrent initialization and shutdown with one task each
    var initTasks = Enumerable.Range(0, 1).Select(_ =>
        Task.Run(() => multiProvider.InitializeAsync(Arg.Any<EvaluationContext>(), CancellationToken.None)));

    var shutdownTasks = Enumerable.Range(0, 1).Select(_ =>
        Task.Run(() => multiProvider.ShutdownAsync(CancellationToken.None)));

    await Task.WhenAll(initTasks.Concat(shutdownTasks));

    // Assert: ensure that each provider ends in a valid lifecycle state
    var statuses = GetRegisteredStatuses().ToList();

    Assert.All(statuses, status =>
    {
        Assert.True(
            status is Constant.ProviderStatus.Ready or Constant.ProviderStatus.NotReady,
            $"Unexpected provider status: {status}");
    });

    // Local helper: uses reflection to access the private '_registeredProviders' field 
    // and retrieve the current status of each registered provider.
    // Consider replacing this with an internal or public method if testing becomes more frequent.
    IEnumerable<Constant.ProviderStatus> GetRegisteredStatuses()
    {
        var field = typeof(MultiProvider).GetField("_registeredProviders", BindingFlags.NonPublic | BindingFlags.Instance);
        if (field?.GetValue(multiProvider) is not IEnumerable<object> list)
            throw new InvalidOperationException("Could not retrieve registered providers via reflection.");

        foreach (var p in list)
        {
            var statusProperty = p.GetType().GetProperty("Status", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (statusProperty == null)
                throw new InvalidOperationException($"'Status' property not found on type {p.GetType().Name}.");

            if (statusProperty.GetValue(p) is not Constant.ProviderStatus status)
                throw new InvalidOperationException("Unable to read status property value.");

            yield return status;
        }
    }
}  

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed with: 4df6c76

Please let me know if I overdid the implementation of a thread safety for the initialisation and shutdown. I added a lock to the ChildProviderStatus and a semaphore for the lifecycle methods.

defaultValue,
ErrorType.General,
Reason.Error,
errorMessage: ex.Message);
Copy link

Choose a reason for hiding this comment

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

Do we want to use ex.ToString()?

try
{
await rp.Provider.InitializeAsync(context, cancellationToken).ConfigureAwait(false);
rp.SetStatus(Constant.ProviderStatus.Ready);
Copy link
Contributor

@arttonoyan arttonoyan Jul 27, 2025

Choose a reason for hiding this comment

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

InitializeAsync and ShutdownAsync iterate through _registeredProviders and update each provider’s status concurrently. Each RegisteredProvider instance’s Status property is mutated inside Task.WhenAll without synchronization

Updating shared mutable state in parallel can lead to data races if the Status property is not thread‑safe. Either update statuses sequentially, protect them with synchronization (e.g., locks) or redesign RegisteredProvider to be immutable and return a new instance with the updated status.


To ensure we don’t run into issues here, I would suggest consider adding an integration test that validates the concurrent status updates behave correctly. This will help verify thread-safety and prevent potential data races.

@askpt askpt force-pushed the askpt/487-add-multi-provider-support branch from e35c0a5 to 78acd86 Compare July 28, 2025 16:05
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.

Add multi-provider support
5 participants