Skip to content

Commit fa5eb92

Browse files
authored
feat(openfeature): add support for OpenFeature flag evaluation and integration (#3)
* feat(openfeature): add support for OpenFeature flag evaluation and integration - Implemented OpenFeature selection mode for trial evaluation. - Added methods for generating OpenFeature flag keys using kebab-case. - Updated documentation to include OpenFeature integration details. - Enhanced existing tests to cover OpenFeature scenarios.
1 parent 7721e95 commit fa5eb92

27 files changed

+693
-62
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
env:
1010
DOTNET_NOLOGO: true
1111
INCLUDE_SYMBOLS: true
12+
MSBUILDDISABLENODEREUSE: 1
1213

1314
jobs:
1415
pr-checks:

Directory.Build.props

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Project>
2+
<PropertyGroup>
3+
<!-- Disable shared compilation to prevent file locking issues with source generators -->
4+
<UseSharedCompilation>false</UseSharedCompilation>
5+
</PropertyGroup>
6+
</Project>

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A .NET library for routing service calls through configurable trials based on fe
99
- Configuration values (string variants)
1010
- Variant feature flags (IVariantFeatureManager integration)
1111
- Sticky routing (deterministic user/session-based routing)
12+
- OpenFeature (open standard for feature flag management)
1213

1314
**Resilience**
1415
- Timeout enforcement with fallback
@@ -164,6 +165,24 @@ c.UsingStickyRouting()
164165
.AddTrial<VariantB>("b")
165166
```
166167

168+
### OpenFeature
169+
Routes based on OpenFeature flag evaluation (works with any OpenFeature-compatible provider):
170+
```csharp
171+
// Install OpenFeature SDK and your preferred provider
172+
// dotnet add package OpenFeature
173+
174+
// Configure provider
175+
await Api.Instance.SetProviderAsync(new YourProvider());
176+
177+
// Configure experiment
178+
c.UsingOpenFeature("payment-processor")
179+
.AddDefaultTrial<StripeProcessor>("stripe")
180+
.AddTrial<PayPalProcessor>("paypal")
181+
.AddTrial<SquareProcessor>("square")
182+
```
183+
184+
See [OpenFeature Integration Guide](docs/user-guide/openfeature.md) for provider setup examples.
185+
167186
## Error Policies
168187

169188
Control fallback behavior when trials fail:
@@ -500,6 +519,7 @@ IMyDatabase (Proxy)
500519
│ - Configuration │
501520
│ - Variant │
502521
│ - Sticky Routing │
522+
│ - OpenFeature │
503523
├─────────────────────────────┤
504524
│ Decorator Pipeline │
505525
│ - Benchmarks │
@@ -653,6 +673,7 @@ All async and generic scenarios validated with comprehensive tests:
653673
| `UsingConfigurationKey(string?)` | Configuration value selection |
654674
| `UsingVariantFeatureFlag(string?)` | Variant feature manager selection |
655675
| `UsingStickyRouting(string?)` | Sticky routing selection |
676+
| `UsingOpenFeature(string?)` | OpenFeature flag selection |
656677
| `AddDefaultTrial<TImpl>(string)` | Registers default trial |
657678
| `AddTrial<TImpl>(string)` | Registers additional trial |
658679
| `OnErrorRedirectAndReplayDefault()` | Falls back to default on error |

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ ExperimentFramework allows you to run experiments by routing service calls to di
1212

1313
## Key Capabilities
1414

15-
- **Multiple Selection Modes**: Route traffic using boolean feature flags, configuration values, variant flags, or deterministic user hashing
15+
- **Multiple Selection Modes**: Route traffic using boolean feature flags, configuration values, variant flags, deterministic user hashing, or OpenFeature providers
1616
- **Error Handling**: Built-in fallback strategies when experimental implementations fail
1717
- **Observability**: Integrated telemetry with OpenTelemetry support for tracking experiment execution
1818
- **Type-Safe**: Strongly-typed fluent API with compile-time validation

docs/user-guide/index.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ ExperimentFramework uses several .NET features to enable runtime experimentation
6060

6161
**Trial Key**: A string identifier for a trial. For boolean feature flags, these are typically "true" and "false". For other modes, they can be any string value.
6262

63-
**Selection Mode**: The strategy used to choose which trial to execute. The framework supports four modes:
64-
- Boolean Feature Flag
65-
- Configuration Value
66-
- Variant Feature Flag
67-
- Sticky Routing
63+
**Selection Mode**: The strategy used to choose which trial to execute. The framework supports five modes:
64+
- Boolean Feature Flag (Microsoft Feature Management)
65+
- Configuration Value (IConfiguration)
66+
- Variant Feature Flag (Microsoft Feature Management variants)
67+
- Sticky Routing (deterministic user-based routing)
68+
- OpenFeature (open standard for feature flags)
6869

6970
**Proxy**: A dynamically generated type that implements your service interface and delegates calls to the selected trial.
7071

@@ -128,4 +129,5 @@ The framework follows these design principles:
128129

129130
- [Getting Started](getting-started.md) - Install the framework and create your first experiment
130131
- [Core Concepts](core-concepts.md) - Detailed explanation of trials, proxies, and decorators
131-
- [Selection Modes](selection-modes.md) - Learn about the four ways to select trials
132+
- [Selection Modes](selection-modes.md) - Learn about the different ways to select trials
133+
- [OpenFeature Integration](openfeature.md) - Use OpenFeature providers for flag management

docs/user-guide/openfeature.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# OpenFeature Integration
2+
3+
ExperimentFramework supports [OpenFeature](https://openfeature.dev/), an open standard for feature flag management. This allows integration with any OpenFeature-compatible provider such as LaunchDarkly, Flagsmith, CloudBees, or custom providers.
4+
5+
## Configuration
6+
7+
Use `UsingOpenFeature()` to configure an experiment to use OpenFeature for trial selection:
8+
9+
```csharp
10+
services.AddExperimentFramework(
11+
ExperimentFrameworkBuilder.Create()
12+
.Define<IPaymentProcessor>(c => c
13+
.UsingOpenFeature("payment-processor-experiment")
14+
.AddDefaultTrial<StripeProcessor>("stripe")
15+
.AddTrial<PayPalProcessor>("paypal")
16+
.AddTrial<SquareProcessor>("square"))
17+
.UseDispatchProxy());
18+
```
19+
20+
## Flag Key Naming
21+
22+
When no flag key is specified, the framework generates a kebab-case name from the service type:
23+
24+
| Service Type | Generated Flag Key |
25+
|--------------|-------------------|
26+
| `IPaymentProcessor` | `payment-processor` |
27+
| `IUserService` | `user-service` |
28+
| `IDataRepository` | `data-repository` |
29+
30+
You can override this with an explicit flag key:
31+
32+
```csharp
33+
.UsingOpenFeature("my-custom-flag-key")
34+
```
35+
36+
## Boolean vs String Flags
37+
38+
The framework automatically detects the flag type based on trial keys:
39+
40+
**Boolean flags** (when trials are "true" and "false"):
41+
42+
```csharp
43+
.Define<IFeature>(c => c
44+
.UsingOpenFeature("new-feature")
45+
.AddDefaultTrial<LegacyFeature>("false")
46+
.AddTrial<NewFeature>("true"))
47+
```
48+
49+
Uses `GetBooleanValueAsync()` from OpenFeature.
50+
51+
**String flags** (multi-variant):
52+
53+
```csharp
54+
.Define<IAlgorithm>(c => c
55+
.UsingOpenFeature("algorithm-variant")
56+
.AddDefaultTrial<ControlAlgorithm>("control")
57+
.AddTrial<VariantA>("variant-a")
58+
.AddTrial<VariantB>("variant-b"))
59+
```
60+
61+
Uses `GetStringValueAsync()` from OpenFeature.
62+
63+
## Provider Setup
64+
65+
Configure your OpenFeature provider before using ExperimentFramework:
66+
67+
```csharp
68+
// Example with InMemoryProvider for testing
69+
await Api.Instance.SetProviderAsync(new InMemoryProvider(new Dictionary<string, Flag>
70+
{
71+
{ "payment-processor-experiment", new Flag<string>("paypal") },
72+
{ "new-feature", new Flag<bool>(true) }
73+
}));
74+
```
75+
76+
For production, use your preferred provider:
77+
78+
```csharp
79+
// LaunchDarkly example
80+
await Api.Instance.SetProviderAsync(
81+
new LaunchDarklyProvider(Configuration.Builder("sdk-key").Build()));
82+
83+
// Flagsmith example
84+
await Api.Instance.SetProviderAsync(
85+
new FlagsmithProvider(new FlagsmithConfiguration { ApiUrl = "..." }));
86+
```
87+
88+
## Fallback Behavior
89+
90+
When OpenFeature is not configured or flag evaluation fails, the framework falls back to the default trial. This provides resilience during:
91+
92+
- Provider initialization
93+
- Network failures
94+
- Missing flag definitions
95+
- Invalid flag values
96+
97+
## Evaluation Context
98+
99+
OpenFeature evaluation context can be set globally or per-client. The framework uses the default client context:
100+
101+
```csharp
102+
// Set global context
103+
Api.Instance.SetContext(new EvaluationContextBuilder()
104+
.Set("userId", "user-123")
105+
.Set("region", "us-east-1")
106+
.Build());
107+
```
108+
109+
## Soft Dependency
110+
111+
OpenFeature is a soft dependency - the framework uses reflection to access OpenFeature APIs. This means:
112+
113+
- No compile-time dependency on the OpenFeature package
114+
- Graceful fallback when OpenFeature is not installed
115+
- Works with any OpenFeature SDK version
116+
117+
To use OpenFeature, add the package to your project:
118+
119+
```bash
120+
dotnet add package OpenFeature
121+
```
122+
123+
## Example: Complete Setup
124+
125+
```csharp
126+
// Program.cs
127+
var builder = WebApplication.CreateBuilder(args);
128+
129+
// Configure OpenFeature provider
130+
await Api.Instance.SetProviderAsync(new YourProvider());
131+
132+
// Configure experiments
133+
builder.Services.AddExperimentFramework(
134+
ExperimentFrameworkBuilder.Create()
135+
.Define<IRecommendationEngine>(c => c
136+
.UsingOpenFeature("recommendation-algorithm")
137+
.AddDefaultTrial<CollaborativeFiltering>("collaborative")
138+
.AddTrial<ContentBased>("content-based")
139+
.AddTrial<HybridApproach>("hybrid")
140+
.OnErrorRedirectAndReplayDefault())
141+
.UseDispatchProxy());
142+
143+
var app = builder.Build();
144+
```
145+
146+
## Combining with Other Selection Modes
147+
148+
You can use different selection modes for different services in the same application:
149+
150+
```csharp
151+
ExperimentFrameworkBuilder.Create()
152+
// OpenFeature for external flag management
153+
.Define<IPaymentProcessor>(c => c
154+
.UsingOpenFeature("payment-experiment")
155+
.AddDefaultTrial<StripeProcessor>("stripe")
156+
.AddTrial<PayPalProcessor>("paypal"))
157+
158+
// Microsoft Feature Management for internal flags
159+
.Define<ISearchService>(c => c
160+
.UsingFeatureFlag("SearchV2")
161+
.AddDefaultTrial<LegacySearch>("false")
162+
.AddTrial<NewSearch>("true"))
163+
164+
// Configuration for static routing
165+
.Define<ILogger>(c => c
166+
.UsingConfigurationKey("Logging:Provider")
167+
.AddDefaultTrial<ConsoleLogger>("console")
168+
.AddTrial<FileLogger>("file"))
169+
```

docs/user-guide/selection-modes.md

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Selection Modes
22

3-
Selection modes determine how the framework chooses which trial to execute for each method call. The framework supports four selection modes, each suited to different use cases.
3+
Selection modes determine how the framework chooses which trial to execute for each method call. The framework supports five selection modes, each suited to different use cases.
44

55
## Overview
66

@@ -10,6 +10,7 @@ Selection modes determine how the framework chooses which trial to execute for e
1010
| Configuration Value | Multi-variant selection | IConfiguration key value |
1111
| Variant Feature Flag | Targeted rollouts | IVariantFeatureManager variant name |
1212
| Sticky Routing | A/B testing by user | Hash of user identity |
13+
| OpenFeature | External flag management | OpenFeature provider evaluation |
1314

1415
## Boolean Feature Flag
1516

@@ -519,6 +520,66 @@ public class TenantUserIdentityProvider : IExperimentIdentityProvider
519520

520521
This ensures users in different tenants can be assigned to different trials.
521522

523+
## OpenFeature
524+
525+
OpenFeature integration allows routing based on any OpenFeature-compatible feature flag provider.
526+
527+
### When to Use
528+
529+
- Using external feature flag services (LaunchDarkly, Flagsmith, CloudBees, etc.)
530+
- Standardized feature flag management across multiple platforms
531+
- Vendor-agnostic feature flag evaluation
532+
- Existing OpenFeature infrastructure
533+
534+
### Configuration
535+
536+
Define the experiment using `UsingOpenFeature()`:
537+
538+
```csharp
539+
var experiments = ExperimentFrameworkBuilder.Create()
540+
.Define<IPaymentProcessor>(c => c
541+
.UsingOpenFeature("payment-processor")
542+
.AddDefaultTrial<StripeProcessor>("stripe")
543+
.AddTrial<PayPalProcessor>("paypal")
544+
.AddTrial<SquareProcessor>("square"));
545+
546+
services.AddExperimentFramework(experiments);
547+
```
548+
549+
### Provider Setup
550+
551+
Configure your OpenFeature provider before using the framework:
552+
553+
```csharp
554+
// Configure provider at startup
555+
await Api.Instance.SetProviderAsync(new YourOpenFeatureProvider());
556+
```
557+
558+
### Flag Key Naming
559+
560+
When no flag key is specified, the framework generates a kebab-case name:
561+
562+
| Service Type | Generated Flag Key |
563+
|--------------|-------------------|
564+
| `IPaymentProcessor` | `payment-processor` |
565+
| `IUserService` | `user-service` |
566+
567+
### Boolean vs String Flags
568+
569+
The framework automatically detects the flag type:
570+
571+
**Boolean flags** (trials are "true" and "false"):
572+
- Uses `GetBooleanValueAsync()`
573+
574+
**String flags** (multi-variant):
575+
- Uses `GetStringValueAsync()`
576+
577+
### Fallback Behavior
578+
579+
If OpenFeature is not configured or evaluation fails, the default trial is used.
580+
581+
For detailed configuration and provider examples, see the [OpenFeature Integration Guide](openfeature.md).
582+
522583
## Choosing a Selection Mode
523584

524585
Use this decision tree to choose the right selection mode:
@@ -527,12 +588,15 @@ Use this decision tree to choose the right selection mode:
527588
Do you need user-specific consistency?
528589
├─ Yes: Use Sticky Routing
529590
└─ No:
530-
└─ How many variants?
531-
├─ Two: Use Boolean Feature Flag
532-
└─ More than two:
533-
└─ Need advanced targeting (user segments, groups, etc.)?
534-
├─ Yes: Use Variant Feature Flag
535-
└─ No: Use Configuration Value
591+
└─ Using external feature flag service (LaunchDarkly, Flagsmith, etc.)?
592+
├─ Yes: Use OpenFeature
593+
└─ No:
594+
└─ How many variants?
595+
├─ Two: Use Boolean Feature Flag
596+
└─ More than two:
597+
└─ Need advanced targeting (user segments, groups, etc.)?
598+
├─ Yes: Use Variant Feature Flag
599+
└─ No: Use Configuration Value
536600
```
537601

538602
## Combining Multiple Experiments

docs/user-guide/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
href: core-concepts.md
77
- name: Selection Modes
88
href: selection-modes.md
9+
- name: OpenFeature Integration
10+
href: openfeature.md
911
- name: Error Handling
1012
href: error-handling.md
1113
- name: Telemetry

src/ExperimentFramework.Generators/Analyzers/DefineCallParser.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ internal static class DefineCallParser
125125
selectorName = ExtractStringArgument(invocation, 0);
126126
break;
127127

128+
case "UsingOpenFeature":
129+
selectionMode = SelectionModeModel.OpenFeature;
130+
selectorName = ExtractStringArgument(invocation, 0);
131+
break;
132+
128133
case "AddDefaultTrial":
129134
var defaultTrialType = ExtractGenericTypeArgument(invocation, semanticModel);
130135
var defaultTrialKey = ExtractStringArgument(invocation, 0);

0 commit comments

Comments
 (0)