@@ -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>();
4048builder .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
4757public static ExperimentFrameworkBuilder ConfigureExperiments ()
4858{
4959 return ExperimentFrameworkBuilder .Create ()
@@ -65,7 +75,25 @@ var experiments = ConfigureExperiments();
6575builder .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
7199public class MyService
@@ -141,15 +169,65 @@ c.UsingStickyRouting()
141169
142170Control 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
2683512 . The generator analyzes ` Define<T>() ` calls to extract interface types
2693523 . For each interface, a proxy class is generated implementing direct method calls
2703534 . 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
273377When you call ` AddExperimentFramework() ` :
2743781 . 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
0 commit comments