From af8c70a188890e4ddd90320d30444d5f3e32ad11 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Thu, 5 Jun 2025 16:42:26 +0100
Subject: [PATCH 01/61] feat: implement MultiProvider class with placeholder
methods for feature resolution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
src/OpenFeature/Extensions/MultiProvider.cs | 34 +++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 src/OpenFeature/Extensions/MultiProvider.cs
diff --git a/src/OpenFeature/Extensions/MultiProvider.cs b/src/OpenFeature/Extensions/MultiProvider.cs
new file mode 100644
index 00000000..185f8339
--- /dev/null
+++ b/src/OpenFeature/Extensions/MultiProvider.cs
@@ -0,0 +1,34 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.Extensions;
+
+public sealed class MultiProvider : FeatureProvider
+{
+ ///
+ public override Metadata? GetMetadata() => new("OpenFeature MultiProvider");
+
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+}
From 6467b7599604a6f37660b9348056fc330a132284 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:00:07 +0100
Subject: [PATCH 02/61] feat: add BaseEvaluationStrategy and MultiProvider
classes for multi-provider feature flag evaluation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/BaseEvaluationStrategy.cs | 27 +++++++++++++++++++
.../{ => MultiProvider}/MultiProvider.cs | 16 ++++++++++-
2 files changed, 42 insertions(+), 1 deletion(-)
create mode 100644 src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs
rename src/OpenFeature/Extensions/{ => MultiProvider}/MultiProvider.cs (67%)
diff --git a/src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs
new file mode 100644
index 00000000..b416415a
--- /dev/null
+++ b/src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs
@@ -0,0 +1,27 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.Extensions.MultiProvider;
+
+///
+/// Provides a base class for implementing evaluation strategies that determine how feature flags are evaluated across multiple feature providers.
+///
+///
+/// This abstract class serves as the foundation for creating custom evaluation strategies that can handle feature flag resolution
+/// across multiple providers. Implementations define the specific logic for how providers are selected, prioritized, or combined
+/// when evaluating feature flags.
+///
+/// Multi Provider specification
+public abstract class BaseEvaluationStrategy
+{
+ ///
+ /// Evaluates a feature flag across multiple providers using the strategy's specific logic.
+ ///
+ /// The type of the feature flag value to be evaluated.
+ /// A dictionary of feature providers keyed by their identifier.
+ /// The feature flag key to evaluate.
+ /// The default value to return if evaluation fails or the flag is not found.
+ /// Optional context information for the evaluation.
+ /// A cancellation token to cancel the operation if needed.
+ /// A task that represents the asynchronous evaluation operation, containing the resolution details with the evaluated value and metadata.
+ public abstract Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default);
+}
diff --git a/src/OpenFeature/Extensions/MultiProvider.cs b/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
similarity index 67%
rename from src/OpenFeature/Extensions/MultiProvider.cs
rename to src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
index 185f8339..e27b154e 100644
--- a/src/OpenFeature/Extensions/MultiProvider.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
@@ -1,32 +1,46 @@
using OpenFeature.Model;
-namespace OpenFeature.Extensions;
+namespace OpenFeature.Extensions.MultiProvider;
+///
+/// A feature provider that enables the use of multiple underlying providers, allowing different providers
+/// to be used for different flag keys or based on specific routing logic.
+///
+///
+/// The MultiProvider acts as a composite provider that can delegate flag resolution to different
+/// underlying providers based on configuration or routing rules. This enables scenarios where
+/// different feature flags may be served by different sources or providers within the same application.
+///
public sealed class MultiProvider : FeatureProvider
{
///
public override Metadata? GetMetadata() => new("OpenFeature MultiProvider");
+ ///
public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
+ ///
public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
+ ///
public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
+ ///
public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
+ ///
public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
From 0a69cee0b8a405e8213f0516b495114050ccbb0f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Thu, 5 Jun 2025 17:27:12 +0100
Subject: [PATCH 03/61] feat: implement MultiProvider methods for feature flag
resolution using evaluation strategy
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../Extensions/MultiProvider/MultiProvider.cs | 44 ++++++++++---------
1 file changed, 24 insertions(+), 20 deletions(-)
diff --git a/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs b/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
index e27b154e..bfc15d90 100644
--- a/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
@@ -13,36 +13,40 @@ namespace OpenFeature.Extensions.MultiProvider;
///
public sealed class MultiProvider : FeatureProvider
{
+ private readonly Dictionary _providers;
+ private readonly BaseEvaluationStrategy _evaluationStrategy;
+
+ ///
+ /// Initializes a new instance of the class with the specified providers and evaluation strategy.
+ ///
+ /// A dictionary containing the feature providers keyed by their identifiers.
+ /// The base evaluation strategy to use for determining how to evaluate features across multiple providers.
+ public MultiProvider(Dictionary providers, BaseEvaluationStrategy evaluationStrategy)
+ {
+ this._providers = providers;
+ this._evaluationStrategy = evaluationStrategy;
+ }
+
///
public override Metadata? GetMetadata() => new("OpenFeature MultiProvider");
///
- public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException();
- }
+ public override Task> ResolveBooleanValueAsync(string flagKey, bool defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this._evaluationStrategy.EvaluateAsync(this._providers, flagKey, defaultValue, context, cancellationToken);
///
- public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException();
- }
+ public override Task> ResolveDoubleValueAsync(string flagKey, double defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this._evaluationStrategy.EvaluateAsync(this._providers, flagKey, defaultValue, context, cancellationToken);
///
- public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException();
- }
+ public override Task> ResolveIntegerValueAsync(string flagKey, int defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this._evaluationStrategy.EvaluateAsync(this._providers, flagKey, defaultValue, context, cancellationToken);
///
- public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException();
- }
+ public override Task> ResolveStringValueAsync(string flagKey, string defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this._evaluationStrategy.EvaluateAsync(this._providers, flagKey, defaultValue, context, cancellationToken);
///
- public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException();
- }
+ public override Task> ResolveStructureValueAsync(string flagKey, Value defaultValue, EvaluationContext? context = null, CancellationToken cancellationToken = default) =>
+ this._evaluationStrategy.EvaluateAsync(this._providers, flagKey, defaultValue, context, cancellationToken);
}
From 0f00aa5bd0d56c942e1ff43218f086ad2e577f73 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 07:38:54 +0100
Subject: [PATCH 04/61] feat: add ComparisonStrategy, FirstMatchStrategy, and
FirstSuccessfulStrategy classes for feature flag evaluation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/BaseEvaluationStrategy.cs | 1 -
.../MultiProvider/ComparisonStrategy.cs | 18 ++++++++++++++++++
.../MultiProvider/FirstMatchStrategy.cs | 18 ++++++++++++++++++
.../MultiProvider/FirstSuccessfulStrategy.cs | 17 +++++++++++++++++
.../Extensions/MultiProvider/MultiProvider.cs | 1 +
5 files changed, 54 insertions(+), 1 deletion(-)
create mode 100644 src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs
create mode 100644 src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
create mode 100644 src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
diff --git a/src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs
index b416415a..819acec3 100644
--- a/src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/BaseEvaluationStrategy.cs
@@ -10,7 +10,6 @@ namespace OpenFeature.Extensions.MultiProvider;
/// across multiple providers. Implementations define the specific logic for how providers are selected, prioritized, or combined
/// when evaluating feature flags.
///
-/// Multi Provider specification
public abstract class BaseEvaluationStrategy
{
///
diff --git a/src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs
new file mode 100644
index 00000000..a0ff0699
--- /dev/null
+++ b/src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs
@@ -0,0 +1,18 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.Extensions.MultiProvider;
+
+///
+/// Require that all providers agree on a value. If every provider returns a non-error result, and the values do not agree,
+/// the Multi-Provider should return the result from a configurable “fallback” provider. It will also call an optional “onMismatch”
+/// callback that can be used to monitor cases where mismatches of evaluation occurred. Otherwise the value of the result will be
+/// the result of the first provider in precedence order.
+///
+public sealed class ComparisonStrategy : BaseEvaluationStrategy
+{
+ ///
+ public override Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
new file mode 100644
index 00000000..d431c2cb
--- /dev/null
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
@@ -0,0 +1,18 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.Extensions.MultiProvider;
+
+///
+/// Return the first result returned by a provider. Skip providers that indicate they had no value due to FLAG_NOT_FOUND.
+/// In all other cases, use the value returned by the provider. If any provider returns an error result other than FLAG_NOT_FOUND,
+/// the whole evaluation should error and “bubble up” the individual provider’s error in the result.
+/// As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call the rest of the providers.
+///
+public sealed class FirstMatchStrategy : BaseEvaluationStrategy
+{
+ ///
+ public override Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
new file mode 100644
index 00000000..0c06b744
--- /dev/null
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
@@ -0,0 +1,17 @@
+using OpenFeature.Model;
+
+namespace OpenFeature.Extensions.MultiProvider;
+
+///
+/// Return the first result returned by a provider. Errors from evaluated providers do not halt execution.
+/// Instead, it will return the first successful result from a provider.
+/// If no provider successfully responds, it will throw an error result.
+///
+public sealed class FirstSuccessfulStrategy : BaseEvaluationStrategy
+{
+ ///
+ public override Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs b/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
index bfc15d90..c130b0e8 100644
--- a/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/MultiProvider.cs
@@ -11,6 +11,7 @@ namespace OpenFeature.Extensions.MultiProvider;
/// underlying providers based on configuration or routing rules. This enables scenarios where
/// different feature flags may be served by different sources or providers within the same application.
///
+/// Multi Provider specification
public sealed class MultiProvider : FeatureProvider
{
private readonly Dictionary _providers;
From d743d2dc09d8e04adedcada3fd60fc9e69650a18 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 14:28:31 +0100
Subject: [PATCH 05/61] feat: implement EvaluateAsync method in
FirstMatchStrategy for type-specific feature resolution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/FirstMatchStrategy.cs | 25 ++++++++++++++++---
1 file changed, 22 insertions(+), 3 deletions(-)
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
index d431c2cb..1a44b159 100644
--- a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
@@ -1,3 +1,4 @@
+using OpenFeature.Constant;
using OpenFeature.Model;
namespace OpenFeature.Extensions.MultiProvider;
@@ -5,14 +6,32 @@ namespace OpenFeature.Extensions.MultiProvider;
///
/// Return the first result returned by a provider. Skip providers that indicate they had no value due to FLAG_NOT_FOUND.
/// In all other cases, use the value returned by the provider. If any provider returns an error result other than FLAG_NOT_FOUND,
-/// the whole evaluation should error and “bubble up” the individual provider’s error in the result.
+/// the whole evaluation should error and "bubble up" the individual provider's error in the result.
/// As soon as a value is returned by a provider, the rest of the operation should short-circuit and not call the rest of the providers.
///
public sealed class FirstMatchStrategy : BaseEvaluationStrategy
{
///
- public override Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
+ public override async Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
{
- throw new NotImplementedException();
+ foreach (var provider in providers.Values)
+ {
+ var result = typeof(T) switch
+ {
+ { } t when t == typeof(bool) => (ResolutionDetails)(object)await provider.ResolveBooleanValueAsync(key, (bool)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(string) => (ResolutionDetails)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(int) => (ResolutionDetails)(object)await provider.ResolveIntegerValueAsync(key, (int)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(double) => (ResolutionDetails)(object)await provider.ResolveDoubleValueAsync(key, (double)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(Value) => (ResolutionDetails)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ _ => new ResolutionDetails(key, defaultValue, ErrorType.TypeMismatch, Reason.Error, errorMessage: $"Unsupported type: {typeof(T).Name}")
+ };
+
+ if (result.ErrorType is ErrorType.None or not ErrorType.FlagNotFound)
+ {
+ return result;
+ }
+ }
+
+ return new ResolutionDetails(key, defaultValue, ErrorType.FlagNotFound, Reason.Error, errorMessage: "Flag not found in any provider");
}
}
From 9e7ed97b5d2601356aa2c06c95dcaf5ffc968ff4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 14:46:42 +0100
Subject: [PATCH 06/61] feat: enhance error handling in FirstMatchStrategy for
feature flag resolution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../Extensions/MultiProvider/FirstMatchStrategy.cs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
index 1a44b159..db169ec4 100644
--- a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
@@ -26,10 +26,17 @@ public override async Task> EvaluateAsync(Dictionary new ResolutionDetails(key, defaultValue, ErrorType.TypeMismatch, Reason.Error, errorMessage: $"Unsupported type: {typeof(T).Name}")
};
+ // If the result is not FLAG_NOT_FOUND and is not an error, return it
if (result.ErrorType is ErrorType.None or not ErrorType.FlagNotFound)
{
return result;
}
+
+ // If the result is an error other than FLAG_NOT_FOUND, bubble it up
+ if (result.ErrorType is not ErrorType.None and not ErrorType.FlagNotFound)
+ {
+ return result;
+ }
}
return new ResolutionDetails(key, defaultValue, ErrorType.FlagNotFound, Reason.Error, errorMessage: "Flag not found in any provider");
From ddd714e7aa5b9ad59257012ec5b0953386a094a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 14:48:04 +0100
Subject: [PATCH 07/61] feat: implement EvaluateAsync method in
FirstSuccessfulStrategy for multi-type feature resolution
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/FirstSuccessfulStrategy.cs | 24 +++++++++++++++++--
1 file changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
index 0c06b744..e3bc4406 100644
--- a/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
@@ -1,3 +1,4 @@
+using OpenFeature.Constant;
using OpenFeature.Model;
namespace OpenFeature.Extensions.MultiProvider;
@@ -10,8 +11,27 @@ namespace OpenFeature.Extensions.MultiProvider;
public sealed class FirstSuccessfulStrategy : BaseEvaluationStrategy
{
///
- public override Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
+ public override async Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
{
- throw new NotImplementedException();
+ foreach (var provider in providers.Values)
+ {
+ var result = typeof(T) switch
+ {
+ { } t when t == typeof(bool) => (ResolutionDetails)(object)await provider.ResolveBooleanValueAsync(key, (bool)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(string) => (ResolutionDetails)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(int) => (ResolutionDetails)(object)await provider.ResolveIntegerValueAsync(key, (int)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(double) => (ResolutionDetails)(object)await provider.ResolveDoubleValueAsync(key, (double)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(Value) => (ResolutionDetails)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ _ => new ResolutionDetails(key, defaultValue, ErrorType.TypeMismatch, Reason.Error, errorMessage: $"Unsupported type: {typeof(T).Name}")
+ };
+
+ // If the result is not FLAG_NOT_FOUND and is not an error, return it
+ if (result.ErrorType is ErrorType.None or not ErrorType.FlagNotFound)
+ {
+ return result;
+ }
+ }
+
+ return new ResolutionDetails(key, defaultValue, ErrorType.FlagNotFound, Reason.Error, errorMessage: "Flag not found in any provider");
}
}
From fdb2e6351b2a27229d887922b6237d7009275637 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 14:52:59 +0100
Subject: [PATCH 08/61] feat: refactor feature resolution strategies to use
EvaluateAsync method for improved multi-provider support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/FirstMatchStrategy.cs | 10 +--------
.../MultiProvider/FirstSuccessfulStrategy.cs | 10 +--------
.../MultiProvider/ProviderExtensions.cs | 22 +++++++++++++++++++
3 files changed, 24 insertions(+), 18 deletions(-)
create mode 100644 src/OpenFeature/Extensions/MultiProvider/ProviderExtensions.cs
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
index db169ec4..15313eba 100644
--- a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
@@ -16,15 +16,7 @@ public override async Task> EvaluateAsync(Dictionary (ResolutionDetails)(object)await provider.ResolveBooleanValueAsync(key, (bool)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(string) => (ResolutionDetails)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(int) => (ResolutionDetails)(object)await provider.ResolveIntegerValueAsync(key, (int)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(double) => (ResolutionDetails)(object)await provider.ResolveDoubleValueAsync(key, (double)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(Value) => (ResolutionDetails)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- _ => new ResolutionDetails(key, defaultValue, ErrorType.TypeMismatch, Reason.Error, errorMessage: $"Unsupported type: {typeof(T).Name}")
- };
+ var result = await provider.EvaluateAsync(key, defaultValue, evaluationContext, cancellationToken).ConfigureAwait(false);
// If the result is not FLAG_NOT_FOUND and is not an error, return it
if (result.ErrorType is ErrorType.None or not ErrorType.FlagNotFound)
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
index e3bc4406..6287268b 100644
--- a/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
@@ -15,15 +15,7 @@ public override async Task> EvaluateAsync(Dictionary (ResolutionDetails)(object)await provider.ResolveBooleanValueAsync(key, (bool)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(string) => (ResolutionDetails)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(int) => (ResolutionDetails)(object)await provider.ResolveIntegerValueAsync(key, (int)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(double) => (ResolutionDetails)(object)await provider.ResolveDoubleValueAsync(key, (double)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- { } t when t == typeof(Value) => (ResolutionDetails)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
- _ => new ResolutionDetails(key, defaultValue, ErrorType.TypeMismatch, Reason.Error, errorMessage: $"Unsupported type: {typeof(T).Name}")
- };
+ var result = await provider.EvaluateAsync(key, defaultValue, evaluationContext, cancellationToken).ConfigureAwait(false);
// If the result is not FLAG_NOT_FOUND and is not an error, return it
if (result.ErrorType is ErrorType.None or not ErrorType.FlagNotFound)
diff --git a/src/OpenFeature/Extensions/MultiProvider/ProviderExtensions.cs b/src/OpenFeature/Extensions/MultiProvider/ProviderExtensions.cs
new file mode 100644
index 00000000..8dfab356
--- /dev/null
+++ b/src/OpenFeature/Extensions/MultiProvider/ProviderExtensions.cs
@@ -0,0 +1,22 @@
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+namespace OpenFeature.Extensions.MultiProvider;
+
+internal static class ProviderExtensions
+{
+ internal static async Task> EvaluateAsync(this FeatureProvider provider, string key, T defaultValue, EvaluationContext? evaluationContext,
+ CancellationToken cancellationToken)
+ {
+ var result = typeof(T) switch
+ {
+ { } t when t == typeof(bool) => (ResolutionDetails)(object)await provider.ResolveBooleanValueAsync(key, (bool)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(string) => (ResolutionDetails)(object)await provider.ResolveStringValueAsync(key, (string)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(int) => (ResolutionDetails)(object)await provider.ResolveIntegerValueAsync(key, (int)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(double) => (ResolutionDetails)(object)await provider.ResolveDoubleValueAsync(key, (double)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ { } t when t == typeof(Value) => (ResolutionDetails)(object)await provider.ResolveStructureValueAsync(key, (Value)(object)defaultValue!, evaluationContext, cancellationToken).ConfigureAwait(false),
+ _ => new ResolutionDetails(key, defaultValue, ErrorType.TypeMismatch, Reason.Error, errorMessage: $"Unsupported type: {typeof(T).Name}")
+ };
+ return result;
+ }
+}
From 776aa61e1a901f157280c58de05a88964fd31242 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 15:06:49 +0100
Subject: [PATCH 09/61] Removed ComparisonStrategy.cs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/ComparisonStrategy.cs | 18 ------------------
1 file changed, 18 deletions(-)
delete mode 100644 src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs
diff --git a/src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs
deleted file mode 100644
index a0ff0699..00000000
--- a/src/OpenFeature/Extensions/MultiProvider/ComparisonStrategy.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using OpenFeature.Model;
-
-namespace OpenFeature.Extensions.MultiProvider;
-
-///
-/// Require that all providers agree on a value. If every provider returns a non-error result, and the values do not agree,
-/// the Multi-Provider should return the result from a configurable “fallback” provider. It will also call an optional “onMismatch”
-/// callback that can be used to monitor cases where mismatches of evaluation occurred. Otherwise the value of the result will be
-/// the result of the first provider in precedence order.
-///
-public sealed class ComparisonStrategy : BaseEvaluationStrategy
-{
- ///
- public override Task> EvaluateAsync(Dictionary providers, string key, T defaultValue, EvaluationContext? evaluationContext = null, CancellationToken cancellationToken = default)
- {
- throw new NotImplementedException();
- }
-}
From 30f8c94a001d417c58dea8de677e33243d1e2a29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 15:59:14 +0100
Subject: [PATCH 10/61] feat: add unit tests for FirstMatchStrategy and
FirstSuccessfulStrategy to enhance multi-provider support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/FirstMatchStrategy.cs | 6 -
.../MultiProvider/FirstMatchStrategyTests.cs | 302 ++++++++++++++++++
2 files changed, 302 insertions(+), 6 deletions(-)
create mode 100644 test/OpenFeature.Tests/Extensions/MultiProvider/FirstMatchStrategyTests.cs
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
index 15313eba..8afb5f09 100644
--- a/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstMatchStrategy.cs
@@ -23,12 +23,6 @@ public override async Task> EvaluateAsync(Dictionary(key, defaultValue, ErrorType.FlagNotFound, Reason.Error, errorMessage: "Flag not found in any provider");
diff --git a/test/OpenFeature.Tests/Extensions/MultiProvider/FirstMatchStrategyTests.cs b/test/OpenFeature.Tests/Extensions/MultiProvider/FirstMatchStrategyTests.cs
new file mode 100644
index 00000000..9c570de3
--- /dev/null
+++ b/test/OpenFeature.Tests/Extensions/MultiProvider/FirstMatchStrategyTests.cs
@@ -0,0 +1,302 @@
+using AutoFixture;
+using NSubstitute;
+using OpenFeature.Constant;
+using OpenFeature.Extensions.MultiProvider;
+using OpenFeature.Model;
+
+namespace OpenFeature.Tests.Extensions.MultiProvider;
+
+public class FirstMatchStrategyTests
+{
+ private readonly Fixture _fixture = new();
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Return_First_Non_FLAG_NOT_FOUND_Result()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 }, { "provider2", provider2 }
+ };
+
+ var flagNotFoundResult =
+ new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, "FLAG_NOT_FOUND");
+ var successResult = new ResolutionDetails(flagKey, true, ErrorType.None, "STATIC", "variant");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(successResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(successResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Return_FLAG_NOT_FOUND_When_All_Providers_Return_FLAG_NOT_FOUND()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 }, { "provider2", provider2 }
+ };
+
+ var flagNotFoundResult =
+ new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, "FLAG_NOT_FOUND");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(ErrorType.FlagNotFound, result.ErrorType);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Return_First_Result_When_No_FLAG_NOT_FOUND()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 }, { "provider2", provider2 }
+ };
+
+ var result1 = new ResolutionDetails(flagKey, true, ErrorType.None, "STATIC", "variant1");
+ var result2 = new ResolutionDetails(flagKey, false, ErrorType.None, "STATIC", "variant2");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result1);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result2);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(result1, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_String_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var expectedResult = new ResolutionDetails(flagKey, "test-value", ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveStringValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Integer_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var expectedResult = new ResolutionDetails(flagKey, 42, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveIntegerValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Double_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var expectedResult = new ResolutionDetails(flagKey, 3.14, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveDoubleValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Structure_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var structureValue = new Value(new Structure(new Dictionary { { "key", new Value("value") } }));
+ var expectedResult = new ResolutionDetails(flagKey, structureValue, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveStructureValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Bubble_Up_Non_FLAG_NOT_FOUND_Error()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var flagErrorResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.General, "UNKNOWN");
+
+ provider.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagErrorResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(flagErrorResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Continue_On_FLAG_NOT_FOUND_And_Bubble_Other_Errors()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 }, { "provider2", provider2 }
+ };
+
+ var flagNotFoundResult =
+ new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, "FLAG_NOT_FOUND");
+ var flagErrorResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.General, "UNKNOWN");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagErrorResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(flagErrorResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Respect_Provider_Order()
+ {
+ // Arrange
+ var strategy = new FirstMatchStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 }, { "provider2", provider2 }
+ };
+
+ var result1 = new ResolutionDetails(flagKey, true, ErrorType.None, "STATIC", "variant1");
+ var result2 = new ResolutionDetails(flagKey, false, ErrorType.None, "STATIC", "variant2");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result1);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result2);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(result1, result);
+ await provider1.Received(1).ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken);
+ await provider2.DidNotReceive().ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken);
+ }
+}
From b12a3cb936afb0698eaf8a3bf95b98c1627f4906 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 16:11:02 +0100
Subject: [PATCH 11/61] feat: add unit tests for FirstSuccessfulStrategy to
validate multi-provider evaluation logic
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/FirstSuccessfulStrategy.cs | 4 +-
.../FirstSuccessfulStrategyTests.cs | 308 ++++++++++++++++++
2 files changed, 310 insertions(+), 2 deletions(-)
create mode 100644 test/OpenFeature.Tests/Extensions/MultiProvider/FirstSuccessfulStrategyTests.cs
diff --git a/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
index 6287268b..635ed059 100644
--- a/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
+++ b/src/OpenFeature/Extensions/MultiProvider/FirstSuccessfulStrategy.cs
@@ -17,8 +17,8 @@ public override async Task> EvaluateAsync(Dictionary();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 },
+ { "provider2", provider2 }
+ };
+
+ var flagNotFoundResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, "FLAG_NOT_FOUND");
+ var successResult = new ResolutionDetails(flagKey, true, ErrorType.None, "STATIC", "variant");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(successResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(successResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Return_FLAG_NOT_FOUND_When_All_Providers_Return_FLAG_NOT_FOUND()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 },
+ { "provider2", provider2 }
+ };
+
+ var flagNotFoundResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, "FLAG_NOT_FOUND");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(ErrorType.FlagNotFound, result.ErrorType);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Return_First_Result_When_No_FLAG_NOT_FOUND()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 },
+ { "provider2", provider2 }
+ };
+
+ var result1 = new ResolutionDetails(flagKey, true, ErrorType.None, "STATIC", "variant1");
+ var result2 = new ResolutionDetails(flagKey, false, ErrorType.None, "STATIC", "variant2");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result1);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result2);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(result1, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_String_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var expectedResult = new ResolutionDetails(flagKey, "test-value", ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveStringValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Integer_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var expectedResult = new ResolutionDetails(flagKey, 42, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveIntegerValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Double_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var expectedResult = new ResolutionDetails(flagKey, 3.14, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveDoubleValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Structure_Should_Work_Correctly()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var structureValue = new Value(new Structure(new Dictionary { { "key", new Value("value") } }));
+ var expectedResult = new ResolutionDetails(flagKey, structureValue, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveStructureValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Continue_FLAG_NOT_FOUND_Error()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "provider", provider } };
+
+ var flagNotFoundResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, "FLAG_NOT_FOUND");
+
+ provider.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(ErrorType.FlagNotFound, result.ErrorType);
+ Assert.Equal(Reason.Error, result.Reason);
+ Assert.Equal("Flag not found in any provider", result.ErrorMessage);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Continue_On_FLAG_NOT_FOUND_And_Continue_Other_Errors()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 },
+ { "provider2", provider2 }
+ };
+
+ var flagNotFoundResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.FlagNotFound, "FLAG_NOT_FOUND");
+ var errorResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.TypeMismatch, "TYPE_MISMATCH");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(flagNotFoundResult);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(errorResult);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(ErrorType.FlagNotFound, result.ErrorType);
+ Assert.Equal(Reason.Error, result.Reason);
+ Assert.Equal("Flag not found in any provider", result.ErrorMessage);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Should_Respect_Provider_Order()
+ {
+ // Arrange
+ var strategy = new FirstSuccessfulStrategy();
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider1 = Substitute.For();
+ var provider2 = Substitute.For();
+ var providers = new Dictionary
+ {
+ { "provider1", provider1 },
+ { "provider2", provider2 }
+ };
+
+ var result1 = new ResolutionDetails(flagKey, true, ErrorType.None, "STATIC", "variant1");
+ var result2 = new ResolutionDetails(flagKey, false, ErrorType.None, "STATIC", "variant2");
+
+ provider1.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result1);
+ provider2.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(result2);
+
+ // Act
+ var result = await strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(result1, result);
+ await provider1.Received(1).ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken);
+ await provider2.DidNotReceive().ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken);
+ }
+}
From a2584690fee39ade41f7a9aa725d297006b719f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 16:13:05 +0100
Subject: [PATCH 12/61] feat: add unit tests for MultiProvider and
ProviderExtensions to validate multi-provider functionality
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/ProviderExtensionsTests.cs | 128 ++++++++++++++++++
1 file changed, 128 insertions(+)
create mode 100644 test/OpenFeature.Tests/Extensions/MultiProvider/ProviderExtensionsTests.cs
diff --git a/test/OpenFeature.Tests/Extensions/MultiProvider/ProviderExtensionsTests.cs b/test/OpenFeature.Tests/Extensions/MultiProvider/ProviderExtensionsTests.cs
new file mode 100644
index 00000000..471d1d78
--- /dev/null
+++ b/test/OpenFeature.Tests/Extensions/MultiProvider/ProviderExtensionsTests.cs
@@ -0,0 +1,128 @@
+using AutoFixture;
+using NSubstitute;
+using OpenFeature.Constant;
+using OpenFeature.Extensions.MultiProvider;
+using OpenFeature.Model;
+
+namespace OpenFeature.Tests.Extensions.MultiProvider;
+
+public class ProviderExtensionsTests : ClearOpenFeatureInstanceFixture
+{
+ private readonly Fixture _fixture = new();
+
+ [Fact]
+ public async Task EvaluateAsync_Boolean_Should_Handle_Successful_Evaluation()
+ {
+ // Arrange
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var expectedResult = new ResolutionDetails(flagKey, true, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await provider.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ await provider.Received(1).ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_String_Should_Handle_Successful_Evaluation()
+ {
+ // Arrange
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var expectedResult = new ResolutionDetails(flagKey, "success", ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveStringValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await provider.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ await provider.Received(1).ResolveStringValueAsync(flagKey, defaultValue, context, cancellationToken);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Integer_Should_Handle_Successful_Evaluation()
+ {
+ // Arrange
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var expectedResult = new ResolutionDetails(flagKey, 42, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveIntegerValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await provider.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ await provider.Received(1).ResolveIntegerValueAsync(flagKey, defaultValue, context, cancellationToken);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Double_Should_Handle_Successful_Evaluation()
+ {
+ // Arrange
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var expectedResult = new ResolutionDetails(flagKey, 3.14, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveDoubleValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await provider.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ await provider.Received(1).ResolveDoubleValueAsync(flagKey, defaultValue, context, cancellationToken);
+ }
+
+ [Fact]
+ public async Task EvaluateAsync_Structure_Should_Handle_Successful_Evaluation()
+ {
+ // Arrange
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var provider = Substitute.For();
+ var successValue = new Value(Structure.Builder().Set("key", "value").Build());
+ var expectedResult = new ResolutionDetails(flagKey, successValue, ErrorType.None, "STATIC", "variant");
+
+ provider.ResolveStructureValueAsync(flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ // Act
+ var result = await provider.EvaluateAsync(flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ await provider.Received(1).ResolveStructureValueAsync(flagKey, defaultValue, context, cancellationToken);
+ }
+}
From 8f752706166552b3b89366f5b41bb621c2cc4fea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Silva?=
<2493377+askpt@users.noreply.github.com>
Date: Fri, 6 Jun 2025 16:19:00 +0100
Subject: [PATCH 13/61] feat: add unit tests for MultiProvider to validate
functionality and strategy delegation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: André Silva <2493377+askpt@users.noreply.github.com>
---
.../MultiProvider/MultiProviderTests.cs | 201 ++++++++++++++++++
1 file changed, 201 insertions(+)
create mode 100644 test/OpenFeature.Tests/Extensions/MultiProvider/MultiProviderTests.cs
diff --git a/test/OpenFeature.Tests/Extensions/MultiProvider/MultiProviderTests.cs b/test/OpenFeature.Tests/Extensions/MultiProvider/MultiProviderTests.cs
new file mode 100644
index 00000000..251c4005
--- /dev/null
+++ b/test/OpenFeature.Tests/Extensions/MultiProvider/MultiProviderTests.cs
@@ -0,0 +1,201 @@
+using AutoFixture;
+using NSubstitute;
+using OpenFeature.Constant;
+using OpenFeature.Model;
+
+using MultiProviderNamespace = OpenFeature.Extensions.MultiProvider;
+
+namespace OpenFeature.Tests.Extensions.MultiProvider;
+
+public class MultiProviderTests : ClearOpenFeatureInstanceFixture
+{
+ private readonly Fixture _fixture = new();
+
+ [Fact]
+ public void MultiProvider_Should_Have_Metadata()
+ {
+ // Arrange
+ var providers = new Dictionary { { "test", new TestProvider() } };
+ var strategy = new MultiProviderNamespace.FirstMatchStrategy();
+ var multiProvider = new MultiProviderNamespace.MultiProvider(providers, strategy);
+
+ // Act
+ var metadata = multiProvider.GetMetadata();
+
+ // Assert
+ Assert.NotNull(metadata);
+ Assert.Equal("OpenFeature MultiProvider", metadata.Name);
+ }
+
+ [Fact]
+ public void Constructor_Should_Set_Properties_Correctly()
+ {
+ // Arrange
+ var providers = new Dictionary { { "test", new TestProvider() } };
+ var strategy = new MultiProviderNamespace.FirstMatchStrategy();
+
+ // Act
+ var multiProvider = new MultiProviderNamespace.MultiProvider(providers, strategy);
+
+ // Assert
+ Assert.NotNull(multiProvider);
+ Assert.NotNull(multiProvider.GetMetadata());
+ Assert.Equal("OpenFeature MultiProvider", multiProvider.GetMetadata()?.Name);
+ }
+
+ [Fact]
+ public async Task ResolveBooleanValueAsync_Should_Delegate_To_Strategy()
+ {
+ // Arrange
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var expectedResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.None, "STATIC", "variant");
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "test", provider } };
+ var strategy = Substitute.For();
+
+ strategy.EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken)
+ .Returns(expectedResult);
+
+ var multiProvider = new MultiProviderNamespace.MultiProvider(providers, strategy);
+
+ // Act
+ var result = await multiProvider.ResolveBooleanValueAsync(flagKey, defaultValue, context, cancellationToken);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ await strategy.Received(1).EvaluateAsync(providers, flagKey, defaultValue, context, cancellationToken);
+ }
+
+ [Fact]
+ public async Task ResolveStringValueAsync_Should_Delegate_To_Strategy()
+ {
+ // Arrange
+ var flagKey = this._fixture.Create();
+ var defaultValue = this._fixture.Create();
+ var context = EvaluationContext.Empty;
+ var cancellationToken = CancellationToken.None;
+
+ var expectedResult = new ResolutionDetails(flagKey, defaultValue, ErrorType.None, "STATIC", "variant");
+
+ var provider = Substitute.For();
+ var providers = new Dictionary { { "test", provider } };
+ var strategy = Substitute.For