Skip to content

Commit 28ae69a

Browse files
committed
feat: implemented additional retry policies allowing for a single or a group of fail-over trials.
feat: Added runtime DispatchProxy with .UseDispatchProxy() docs: added additional documentation for new features. chore: added comprehensive sample app
1 parent 11516ae commit 28ae69a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3636
-70
lines changed

ExperimentFramework.slnx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
<Solution>
2-
<Project Path="samples/ExperimentFramework.SampleWebApp/ExperimentFramework.SampleWebApp.csproj" />
3-
<Project Path="src/ExperimentFramework.Generators/ExperimentFramework.Generators.csproj" />
4-
5-
<Project Path="src/ExperimentFramework/ExperimentFramework.csproj" />
6-
<Project Path="benchmarks/ExperimentFramework.Benchmarks/ExperimentFramework.Benchmarks.csproj" />
7-
<Project Path="samples/ExperimentFramework.SampleConsole/ExperimentFramework.SampleConsole.csproj" />
8-
<Project Path="tests/ExperimentFramework.Tests/ExperimentFramework.Tests.csproj" />
2+
<Folder Name="/samples/">
3+
<Project Path="samples/ExperimentFramework.ComprehensiveSample/ExperimentFramework.ComprehensiveSample.csproj" />
4+
<Project Path="samples/ExperimentFramework.SampleConsole/ExperimentFramework.SampleConsole.csproj" />
5+
<Project Path="samples/ExperimentFramework.SampleWebApp/ExperimentFramework.SampleWebApp.csproj" />
6+
</Folder>
7+
<Folder Name="/src/">
8+
<Project Path="benchmarks/ExperimentFramework.Benchmarks/ExperimentFramework.Benchmarks.csproj" />
9+
<Project Path="src/ExperimentFramework.Generators/ExperimentFramework.Generators.csproj" />
10+
<Project Path="src/ExperimentFramework/ExperimentFramework.csproj" />
11+
</Folder>
12+
<Folder Name="/tests/">
13+
<Project Path="tests/ExperimentFramework.Tests/ExperimentFramework.Tests.csproj" />
14+
</Folder>
915
</Solution>

README.md

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,15 @@ A .NET framework for runtime-switchable A/B testing, feature flags, trial fallba
2929

3030
## Quick Start
3131

32-
### 1. Register Services
32+
### 1. Install Packages
33+
34+
```bash
35+
dotnet add package ExperimentFramework
36+
dotnet add package ExperimentFramework.Generators # For source-generated proxies
37+
# OR use runtime proxies (no generator package needed)
38+
```
39+
40+
### 2. Register Services
3341

3442
```csharp
3543
// Register concrete implementations
@@ -40,10 +48,12 @@ builder.Services.AddScoped<MyCloudDbContext>();
4048
builder.Services.AddScoped<IMyDatabase, MyDbContext>();
4149
```
4250

43-
### 2. Configure Experiments
51+
### 3. Configure Experiments
52+
53+
**Option A: Source-Generated Proxies (Recommended - Fast)**
4454

4555
```csharp
46-
[ExperimentCompositionRoot]
56+
[ExperimentCompositionRoot] // Triggers source generation
4757
public static ExperimentFrameworkBuilder ConfigureExperiments()
4858
{
4959
return ExperimentFrameworkBuilder.Create()
@@ -65,7 +75,25 @@ var experiments = ConfigureExperiments();
6575
builder.Services.AddExperimentFramework(experiments);
6676
```
6777

68-
### 3. Use Services Normally
78+
**Option B: Runtime Proxies (Flexible)**
79+
80+
```csharp
81+
public static ExperimentFrameworkBuilder ConfigureExperiments()
82+
{
83+
return ExperimentFrameworkBuilder.Create()
84+
.Define<IMyDatabase>(c =>
85+
c.UsingFeatureFlag("UseCloudDb")
86+
.AddDefaultTrial<MyDbContext>("false")
87+
.AddTrial<MyCloudDbContext>("true")
88+
.OnErrorRedirectAndReplayDefault())
89+
.UseDispatchProxy(); // Use runtime proxies instead
90+
}
91+
92+
var experiments = ConfigureExperiments();
93+
builder.Services.AddExperimentFramework(experiments);
94+
```
95+
96+
### 4. Use Services Normally
6997

7098
```csharp
7199
public class MyService
@@ -141,15 +169,65 @@ c.UsingStickyRouting()
141169

142170
Control fallback behavior when trials fail:
143171

172+
### 1. Throw (Default)
173+
Exception propagates immediately, no retries:
144174
```csharp
145-
// Throw immediately on error (default)
146-
.OnErrorRedirectAndReplayDefault()
175+
// No method call needed - Throw is the default policy
176+
.Define<IMyService>(c => c
177+
.UsingFeatureFlag("MyFeature")
178+
.AddDefaultTrial<DefaultImpl>("false")
179+
.AddTrial<ExperimentalImpl>("true"))
180+
// If ExperimentalImpl throws, exception propagates to caller
181+
```
147182

148-
// Fall back to default trial on error
149-
.OnErrorRedirectAndReplayDefault()
183+
### 2. RedirectAndReplayDefault
184+
Falls back to default trial on error:
185+
```csharp
186+
.Define<IMyService>(c => c
187+
.UsingFeatureFlag("MyFeature")
188+
.AddDefaultTrial<DefaultImpl>("false")
189+
.AddTrial<ExperimentalImpl>("true")
190+
.OnErrorRedirectAndReplayDefault())
191+
// Tries: [preferred, default]
192+
```
193+
194+
### 3. RedirectAndReplayAny
195+
Tries all trials until one succeeds (sorted alphabetically):
196+
```csharp
197+
.Define<IMyService>(c => c
198+
.UsingConfigurationKey("ServiceVariant")
199+
.AddDefaultTrial<DefaultImpl>("")
200+
.AddTrial<VariantA>("a")
201+
.AddTrial<VariantB>("b")
202+
.OnErrorRedirectAndReplayAny())
203+
// Tries all variants in sorted order until one succeeds
204+
```
150205

151-
// Try all trials until one succeeds
152-
.OnErrorRedirectAndReplayAny()
206+
### 4. RedirectAndReplay
207+
Redirects to a specific fallback trial (e.g., Noop diagnostics handler):
208+
```csharp
209+
.Define<IMyService>(c => c
210+
.UsingFeatureFlag("MyFeature")
211+
.AddDefaultTrial<PrimaryImpl>("true")
212+
.AddTrial<SecondaryImpl>("false")
213+
.AddTrial<NoopHandler>("noop")
214+
.OnErrorRedirectAndReplay("noop"))
215+
// Tries: [preferred, specific_fallback]
216+
// Useful for dedicated diagnostics/safe-mode handlers
217+
```
218+
219+
### 5. RedirectAndReplayOrdered
220+
Tries ordered list of fallback trials:
221+
```csharp
222+
.Define<IMyService>(c => c
223+
.UsingFeatureFlag("UseCloudDb")
224+
.AddDefaultTrial<CloudDbImpl>("true")
225+
.AddTrial<LocalCacheImpl>("cache")
226+
.AddTrial<InMemoryCacheImpl>("memory")
227+
.AddTrial<StaticDataImpl>("static")
228+
.OnErrorRedirectAndReplayOrdered("cache", "memory", "static"))
229+
// Tries: [preferred, cache, memory, static] in exact order
230+
// Fine-grained control over fallback strategy
153231
```
154232

155233
## Custom Naming Conventions
@@ -262,13 +340,39 @@ Because the JSON file is loaded with `reloadOnChange: true`, changes will be pic
262340

263341
## How It Works
264342

265-
### Source Generation
266-
The framework uses Roslyn source generators to create optimized proxy classes at compile time:
267-
1. The `[ExperimentCompositionRoot]` attribute triggers the generator
343+
### Proxy Generation
344+
345+
The framework supports two proxy modes:
346+
347+
**1. Source-Generated Proxies (Default, Recommended)**
348+
349+
Uses Roslyn source generators to create optimized proxy classes at compile time:
350+
1. The `[ExperimentCompositionRoot]` attribute or `.UseSourceGenerators()` triggers the generator
268351
2. The generator analyzes `Define<T>()` calls to extract interface types
269352
3. For each interface, a proxy class is generated implementing direct method calls
270353
4. Generated proxies are discovered and registered automatically
271354

355+
Performance: <100ns overhead per method call (near-zero reflection overhead)
356+
357+
**2. Runtime Proxies (Alternative)**
358+
359+
Uses `System.Reflection.DispatchProxy` for dynamic proxies:
360+
361+
```csharp
362+
var experiments = ExperimentFrameworkBuilder.Create()
363+
.Define<IMyDatabase>(c => c.UsingFeatureFlag("UseCloudDb")...)
364+
.UseDispatchProxy(); // Use runtime proxies instead of source generation
365+
366+
builder.Services.AddExperimentFramework(experiments);
367+
```
368+
369+
Performance: ~800ns overhead per method call (reflection-based)
370+
371+
Use runtime proxies when:
372+
- Source generators are not available in your build environment
373+
- You need maximum debugging flexibility
374+
- Performance overhead is acceptable for your use case
375+
272376
### DI Rewriting
273377
When you call `AddExperimentFramework()`:
274378
1. Existing interface registrations are removed
@@ -428,9 +532,12 @@ All async and generic scenarios validated with comprehensive tests:
428532
429533
## Important Notes
430534
535+
- **Proxy Mode Selection**: You must choose between source-generated or runtime proxies:
536+
- Source-generated (recommended): Requires `ExperimentFramework.Generators` package + `[ExperimentCompositionRoot]` attribute or `.UseSourceGenerators()` call
537+
- Runtime (alternative): No extra package needed, just call `.UseDispatchProxy()` on the builder
431538
- Trials **must be registered by concrete type** (ImplementationType) in DI. Factory/instance registrations are not supported.
432-
- Source generation requires either `[ExperimentCompositionRoot]` attribute or `.UseSourceGenerators()` fluent API call.
433-
- Generated proxies use direct method calls for zero-reflection overhead.
539+
- Source-generated proxies use direct method calls for zero-reflection overhead (<100ns per call).
540+
- Runtime proxies use `DispatchProxy` with reflection (~800ns per call).
434541
- Variant feature flag support requires reflection to access internal Microsoft.FeatureManagement APIs and may require updates for future versions.
435542
436543
## API Reference
@@ -440,6 +547,8 @@ All async and generic scenarios validated with comprehensive tests:
440547
| Method | Description |
441548
|--------|-------------|
442549
| `Create()` | Creates a new framework builder |
550+
| `UseSourceGenerators()` | Use compile-time source-generated proxies (<100ns overhead) |
551+
| `UseDispatchProxy()` | Use runtime DispatchProxy-based proxies (~800ns overhead) |
443552
| `UseNamingConvention(IExperimentNamingConvention)` | Sets custom naming convention |
444553
| `AddLogger(Action<ExperimentLoggingBuilder>)` | Adds logging decorators |
445554
| `AddDecoratorFactory(IExperimentDecoratorFactory)` | Adds custom decorator |
@@ -457,6 +566,8 @@ All async and generic scenarios validated with comprehensive tests:
457566
| `AddTrial<TImpl>(string)` | Registers additional trial |
458567
| `OnErrorRedirectAndReplayDefault()` | Falls back to default on error |
459568
| `OnErrorRedirectAndReplayAny()` | Tries all trials on error |
569+
| `OnErrorRedirectAndReplay(string)` | Redirects to specific fallback trial on error |
570+
| `OnErrorRedirectAndReplayOrdered(params string[])` | Tries ordered list of fallback trials on error |
460571
461572
### Extension Methods
462573

docs/index.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ Install the ExperimentFramework package via NuGet:
2626
dotnet add package ExperimentFramework
2727
```
2828

29+
**For source-generated proxies (recommended for best performance):**
30+
31+
```bash
32+
dotnet add package ExperimentFramework.Generators
33+
```
34+
35+
Use `[ExperimentCompositionRoot]` attribute or `.UseSourceGenerators()` in your configuration.
36+
37+
**For runtime proxies (alternative, more flexible):**
38+
39+
No additional package needed. Use `.UseDispatchProxy()` in your configuration.
40+
2941
## Quick Example
3042

3143
Define an experiment that switches between database implementations based on a feature flag:
@@ -36,15 +48,32 @@ services.AddScoped<LocalDatabase>();
3648
services.AddScoped<CloudDatabase>();
3749
services.AddScoped<IDatabase, LocalDatabase>();
3850

39-
// Configure the experiment
40-
var experiments = ExperimentFrameworkBuilder.Create()
41-
.Define<IDatabase>(c => c
42-
.UsingFeatureFlag("UseCloudDb")
43-
.AddDefaultTrial<LocalDatabase>("false")
44-
.AddTrial<CloudDatabase>("true")
45-
.OnErrorRedirectAndReplayDefault());
51+
// Configure the experiment (source-generated proxies)
52+
[ExperimentCompositionRoot]
53+
static ExperimentFrameworkBuilder ConfigureExperiments()
54+
{
55+
return ExperimentFrameworkBuilder.Create()
56+
.Define<IDatabase>(c => c
57+
.UsingFeatureFlag("UseCloudDb")
58+
.AddDefaultTrial<LocalDatabase>("false")
59+
.AddTrial<CloudDatabase>("true")
60+
.OnErrorRedirectAndReplayDefault());
61+
}
62+
63+
// OR use runtime proxies
64+
static ExperimentFrameworkBuilder ConfigureWithRuntimeProxies()
65+
{
66+
return ExperimentFrameworkBuilder.Create()
67+
.Define<IDatabase>(c => c
68+
.UsingFeatureFlag("UseCloudDb")
69+
.AddDefaultTrial<LocalDatabase>("false")
70+
.AddTrial<CloudDatabase>("true")
71+
.OnErrorRedirectAndReplayDefault())
72+
.UseDispatchProxy();
73+
}
4674

4775
// Register with dependency injection
76+
var experiments = ConfigureExperiments();
4877
services.AddExperimentFramework(experiments);
4978
```
5079

docs/user-guide/SourceGeneratorGuide.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,86 @@ public class MyService
190190
}
191191
```
192192

193+
## Proxy Modes
194+
195+
The framework supports two proxy generation strategies:
196+
197+
### Source-Generated Proxies (Default, Recommended)
198+
199+
Compile-time code generation using Roslyn source generators:
200+
201+
**Advantages:**
202+
- Near-zero overhead (<100ns per call)
203+
- No reflection at runtime
204+
- AOT compatible
205+
- Full type safety
206+
207+
**Configuration:**
208+
```csharp
209+
[ExperimentCompositionRoot]
210+
public static ExperimentFrameworkBuilder ConfigureExperiments()
211+
{
212+
return ExperimentFrameworkBuilder.Create()
213+
.Define<IMyDatabase>(c => c
214+
.UsingFeatureFlag("UseCloudDb")
215+
.AddDefaultTrial<LocalDatabase>("false")
216+
.AddTrial<CloudDatabase>("true"));
217+
// Source generation triggered by [ExperimentCompositionRoot]
218+
}
219+
```
220+
221+
Or explicitly with fluent API:
222+
```csharp
223+
public static ExperimentFrameworkBuilder ConfigureExperiments()
224+
{
225+
return ExperimentFrameworkBuilder.Create()
226+
.Define<IMyDatabase>(/* ... */)
227+
.UseSourceGenerators(); // Explicit marker
228+
}
229+
```
230+
231+
### Runtime Proxies (Alternative)
232+
233+
Dynamic proxy generation using `System.Reflection.DispatchProxy`:
234+
235+
**Advantages:**
236+
- No source generator required
237+
- Maximum debugging flexibility
238+
- Simpler build process
239+
240+
**Disadvantages:**
241+
- Higher overhead (~800ns per call)
242+
- Reflection-based dispatch
243+
- Not AOT compatible
244+
245+
**Configuration:**
246+
```csharp
247+
public static ExperimentFrameworkBuilder ConfigureExperiments()
248+
{
249+
return ExperimentFrameworkBuilder.Create()
250+
.Define<IMyDatabase>(c => c
251+
.UsingFeatureFlag("UseCloudDb")
252+
.AddDefaultTrial<LocalDatabase>("false")
253+
.AddTrial<CloudDatabase>("true"))
254+
.UseDispatchProxy(); // Use runtime proxies
255+
}
256+
```
257+
258+
**When to use runtime proxies:**
259+
- Source generators are not available in your build environment
260+
- You need maximum debugging flexibility during development
261+
- Performance overhead is acceptable for your use case
262+
- You're prototyping or doing short-term experiments
263+
264+
**Performance Comparison:**
265+
266+
| Operation | Source Generated | Runtime Proxy |
267+
|-----------|------------------|---------------|
268+
| Proxy overhead | <100ns | ~800ns |
269+
| Method invocation | Direct call | Reflection-based |
270+
| Allocations | Minimal | Higher (boxing) |
271+
| AOT compatible | Yes | No |
272+
193273
## How It Works
194274

195275
### Source Generation

0 commit comments

Comments
 (0)