@@ -71,92 +71,148 @@ public FeatureManager(
7171
7272 public Task < bool > IsEnabledAsync ( string feature )
7373 {
74- return IsEnabledWithVariantsAsync < object > ( feature , appContext : null , useAppContext : false , CancellationToken . None ) ;
74+ return IsEnabledEvaluation < object > ( feature , appContext : null , useAppContext : false , CancellationToken . None ) ;
7575 }
7676
7777 public Task < bool > IsEnabledAsync < TContext > ( string feature , TContext appContext )
7878 {
79- return IsEnabledWithVariantsAsync ( feature , appContext , useAppContext : true , CancellationToken . None ) ;
79+ return IsEnabledEvaluation ( feature , appContext , useAppContext : true , CancellationToken . None ) ;
8080 }
8181
8282 public Task < bool > IsEnabledAsync ( string feature , CancellationToken cancellationToken )
8383 {
84- return IsEnabledWithVariantsAsync < object > ( feature , appContext : null , useAppContext : false , cancellationToken ) ;
84+ return IsEnabledEvaluation < object > ( feature , appContext : null , useAppContext : false , cancellationToken ) ;
8585 }
8686
8787 public Task < bool > IsEnabledAsync < TContext > ( string feature , TContext appContext , CancellationToken cancellationToken )
8888 {
89- return IsEnabledWithVariantsAsync ( feature , appContext , useAppContext : true , cancellationToken ) ;
89+ return IsEnabledEvaluation ( feature , appContext , useAppContext : true , cancellationToken ) ;
9090 }
9191
92- private async Task < bool > IsEnabledWithVariantsAsync < TContext > ( string feature , TContext appContext , bool useAppContext , CancellationToken cancellationToken )
92+ private async Task < bool > IsEnabledEvaluation < TContext > ( string feature , TContext appContext , bool useAppContext , CancellationToken cancellationToken )
9393 {
94- bool isFeatureEnabled = false ;
94+ EvaluationEvent evaluationEvent = new EvaluationEvent
95+ {
96+ FeatureDefinition = await GetFeatureDefinition ( feature ) . ConfigureAwait ( false )
97+ } ;
9598
96- FeatureDefinition featureDefinition = await GetFeatureDefinition ( feature ) . ConfigureAwait ( false ) ;
99+ await EvaluateFeature ( evaluationEvent , appContext , useAppContext , cancellationToken ) ;
97100
98- VariantDefinition variantDefinition = null ;
101+ return evaluationEvent . IsEnabled ;
102+ }
99103
100- if ( featureDefinition != null )
104+ public ValueTask < Variant > GetVariantAsync ( string feature , CancellationToken cancellationToken )
105+ {
106+ if ( string . IsNullOrEmpty ( feature ) )
101107 {
102- isFeatureEnabled = await IsEnabledAsync ( featureDefinition , appContext , useAppContext , cancellationToken ) . ConfigureAwait ( false ) ;
108+ throw new ArgumentNullException ( nameof ( feature ) ) ;
109+ }
110+
111+ return GetVariantEvaluation ( feature , context : null , useContext : false , cancellationToken ) ;
112+ }
113+
114+ public ValueTask < Variant > GetVariantAsync ( string feature , TargetingContext context , CancellationToken cancellationToken )
115+ {
116+ if ( string . IsNullOrEmpty ( feature ) )
117+ {
118+ throw new ArgumentNullException ( nameof ( feature ) ) ;
119+ }
120+
121+ if ( context == null )
122+ {
123+ throw new ArgumentNullException ( nameof ( context ) ) ;
124+ }
125+
126+ return GetVariantEvaluation ( feature , context , useContext : true , cancellationToken ) ;
127+ }
103128
104- if ( featureDefinition . Variants != null && featureDefinition . Variants . Any ( ) && featureDefinition . Allocation != null )
129+ private async ValueTask < Variant > GetVariantEvaluation ( string feature , TargetingContext context , bool useContext , CancellationToken cancellationToken )
130+ {
131+ EvaluationEvent evaluationEvent = new EvaluationEvent
132+ {
133+ FeatureDefinition = await GetFeatureDefinition ( feature ) . ConfigureAwait ( false )
134+ } ;
135+
136+ await EvaluateFeature ( evaluationEvent , context , useContext , cancellationToken ) ;
137+
138+ return evaluationEvent . Variant ;
139+ }
140+
141+ private async Task < EvaluationEvent > EvaluateFeature < TContext > ( EvaluationEvent evaluationEvent , TContext context , bool useContext , CancellationToken cancellationToken )
142+ {
143+ if ( evaluationEvent . FeatureDefinition != null )
144+ {
145+ //
146+ // Determine IsEnabled
147+ evaluationEvent . IsEnabled = await IsEnabledAsync ( evaluationEvent . FeatureDefinition , context , useContext , cancellationToken ) . ConfigureAwait ( false ) ;
148+
149+ //
150+ // Determine Variant
151+ VariantDefinition variantDefinition ;
152+
153+ if ( evaluationEvent . FeatureDefinition . Allocation == null || ( ! evaluationEvent . FeatureDefinition . Variants ? . Any ( ) ?? false ) )
154+ {
155+ variantDefinition = null ;
156+
157+ evaluationEvent . VariantReason = "No Allocation or Variants" ;
158+ }
159+ else
105160 {
106- if ( ! isFeatureEnabled )
161+ if ( ! evaluationEvent . IsEnabled )
107162 {
108- variantDefinition = featureDefinition . Variants . FirstOrDefault ( ( variant ) => variant . Name == featureDefinition . Allocation . DefaultWhenDisabled ) ;
163+ variantDefinition = evaluationEvent . FeatureDefinition . Variants . FirstOrDefault ( ( variant ) => variant . Name == evaluationEvent . FeatureDefinition . Allocation . DefaultWhenDisabled ) ;
164+
165+ evaluationEvent . VariantReason = "Disabled Default" ;
109166 }
110167 else
111168 {
112169 TargetingContext targetingContext ;
113170
114- if ( useAppContext )
171+ if ( useContext )
115172 {
116- targetingContext = appContext as TargetingContext ;
173+ targetingContext = context as TargetingContext ;
117174 }
118175 else
119176 {
120177 targetingContext = await ResolveTargetingContextAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
121178 }
122179
123180 variantDefinition = await GetAssignedVariantAsync (
124- featureDefinition ,
181+ evaluationEvent ,
125182 targetingContext ,
126183 cancellationToken )
127184 . ConfigureAwait ( false ) ;
128185 }
129186
130- if ( variantDefinition != null && featureDefinition . Status != FeatureStatus . Disabled )
187+ evaluationEvent . Variant = variantDefinition != null ? GetVariantFromVariantDefinition ( variantDefinition ) : null ;
188+
189+ //
190+ // Override IsEnabled if variant has an override
191+ if ( variantDefinition != null && evaluationEvent . FeatureDefinition . Status != FeatureStatus . Disabled )
131192 {
132193 if ( variantDefinition . StatusOverride == StatusOverride . Enabled )
133194 {
134- isFeatureEnabled = true ;
195+ evaluationEvent . IsEnabled = true ;
135196 }
136197 else if ( variantDefinition . StatusOverride == StatusOverride . Disabled )
137198 {
138- isFeatureEnabled = false ;
199+ evaluationEvent . IsEnabled = false ;
139200 }
140201 }
141202 }
142203 }
143204
144205 foreach ( ISessionManager sessionManager in _sessionManagers )
145206 {
146- await sessionManager . SetAsync ( feature , isFeatureEnabled ) . ConfigureAwait ( false ) ;
207+ await sessionManager . SetAsync ( evaluationEvent . FeatureDefinition . Name , evaluationEvent . IsEnabled ) . ConfigureAwait ( false ) ;
147208 }
148209
149- if ( featureDefinition . TelemetryEnabled )
210+ if ( evaluationEvent . FeatureDefinition . TelemetryEnabled )
150211 {
151- PublishTelemetry ( new EvaluationEvent
152- {
153- FeatureDefinition = featureDefinition ,
154- IsEnabled = isFeatureEnabled ,
155- Variant = variantDefinition != null ? GetVariantFromVariantDefinition ( variantDefinition ) : null
156- } , cancellationToken ) ;
212+ PublishTelemetry ( evaluationEvent , cancellationToken ) ;
157213 }
158214
159- return isFeatureEnabled ;
215+ return evaluationEvent ;
160216 }
161217
162218 public IAsyncEnumerable < string > GetFeatureNamesAsync ( )
@@ -304,73 +360,6 @@ await contextualFilter.EvaluateAsync(context, appContext).ConfigureAwait(false)
304360 return enabled ;
305361 }
306362
307- public ValueTask < Variant > GetVariantAsync ( string feature , CancellationToken cancellationToken )
308- {
309- if ( string . IsNullOrEmpty ( feature ) )
310- {
311- throw new ArgumentNullException ( nameof ( feature ) ) ;
312- }
313-
314- return GetVariantAsync ( feature , context : null , useContext : false , cancellationToken ) ;
315- }
316-
317- public ValueTask < Variant > GetVariantAsync ( string feature , TargetingContext context , CancellationToken cancellationToken )
318- {
319- if ( string . IsNullOrEmpty ( feature ) )
320- {
321- throw new ArgumentNullException ( nameof ( feature ) ) ;
322- }
323-
324- if ( context == null )
325- {
326- throw new ArgumentNullException ( nameof ( context ) ) ;
327- }
328-
329- return GetVariantAsync ( feature , context , useContext : true , cancellationToken ) ;
330- }
331-
332- private async ValueTask < Variant > GetVariantAsync ( string feature , TargetingContext context , bool useContext , CancellationToken cancellationToken )
333- {
334- FeatureDefinition featureDefinition = await GetFeatureDefinition ( feature ) . ConfigureAwait ( false ) ;
335-
336- if ( featureDefinition == null || featureDefinition . Allocation == null || ( ! featureDefinition . Variants ? . Any ( ) ?? false ) )
337- {
338- return null ;
339- }
340-
341- VariantDefinition variantDefinition = null ;
342-
343- bool isFeatureEnabled = await IsEnabledAsync ( featureDefinition , context , useContext , cancellationToken ) . ConfigureAwait ( false ) ;
344-
345- if ( ! isFeatureEnabled )
346- {
347- variantDefinition = featureDefinition . Variants . FirstOrDefault ( ( variant ) => variant . Name == featureDefinition . Allocation . DefaultWhenDisabled ) ;
348- }
349- else
350- {
351- if ( ! useContext )
352- {
353- context = await ResolveTargetingContextAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
354- }
355-
356- variantDefinition = await GetAssignedVariantAsync ( featureDefinition , context , cancellationToken ) . ConfigureAwait ( false ) ;
357- }
358-
359- Variant variant = variantDefinition != null ? GetVariantFromVariantDefinition ( variantDefinition ) : null ;
360-
361- if ( featureDefinition . TelemetryEnabled )
362- {
363- PublishTelemetry ( new EvaluationEvent
364- {
365- FeatureDefinition = featureDefinition ,
366- IsEnabled = isFeatureEnabled ,
367- Variant = variant
368- } , cancellationToken ) ;
369- }
370-
371- return variant ;
372- }
373-
374363 private async ValueTask < FeatureDefinition > GetFeatureDefinition ( string feature )
375364 {
376365 FeatureDefinition featureDefinition = await _featureDefinitionProvider
@@ -415,95 +404,103 @@ private async ValueTask<TargetingContext> ResolveTargetingContextAsync(Cancellat
415404 return context ;
416405 }
417406
418- private async ValueTask < VariantDefinition > GetAssignedVariantAsync ( FeatureDefinition featureDefinition , TargetingContext context , CancellationToken cancellationToken )
407+ private async ValueTask < VariantDefinition > GetAssignedVariantAsync ( EvaluationEvent evaluationEvent , TargetingContext context , CancellationToken cancellationToken )
419408 {
420409 VariantDefinition variantDefinition = null ;
421410
422411 if ( context != null )
423412 {
424- variantDefinition = await AssignVariantAsync ( featureDefinition , context , cancellationToken ) . ConfigureAwait ( false ) ;
413+ variantDefinition = await AssignVariantAsync ( evaluationEvent , context , cancellationToken ) . ConfigureAwait ( false ) ;
425414 }
426415
427416 if ( variantDefinition == null )
428417 {
429- variantDefinition = featureDefinition . Variants . FirstOrDefault ( ( variant ) => variant . Name == featureDefinition . Allocation . DefaultWhenEnabled ) ;
418+ variantDefinition = evaluationEvent . FeatureDefinition . Variants . FirstOrDefault ( ( variant ) => variant . Name == evaluationEvent . FeatureDefinition . Allocation . DefaultWhenEnabled ) ;
419+
420+ evaluationEvent . VariantReason = "Enabled Default" ;
430421 }
431422
432423 return variantDefinition ;
433424 }
434425
435- private ValueTask < VariantDefinition > AssignVariantAsync ( FeatureDefinition featureDefinition , TargetingContext targetingContext , CancellationToken cancellationToken )
426+ private ValueTask < VariantDefinition > AssignVariantAsync ( EvaluationEvent evaluationEvent , TargetingContext targetingContext , CancellationToken cancellationToken )
436427 {
437428 VariantDefinition variant = null ;
438429
439- if ( featureDefinition . Allocation . User != null )
430+ if ( evaluationEvent . FeatureDefinition . Allocation . User != null )
440431 {
441- foreach ( UserAllocation user in featureDefinition . Allocation . User )
432+ foreach ( UserAllocation user in evaluationEvent . FeatureDefinition . Allocation . User )
442433 {
443434 if ( TargetingEvaluator . IsTargeted ( targetingContext . UserId , user . Users , _assignerOptions . IgnoreCase ) )
444435 {
445436 if ( string . IsNullOrEmpty ( user . Variant ) )
446437 {
447- _logger . LogWarning ( $ "Missing variant name for user allocation in feature { featureDefinition . Name } ") ;
438+ _logger . LogWarning ( $ "Missing variant name for user allocation in feature { evaluationEvent . FeatureDefinition . Name } ") ;
448439
449440 return new ValueTask < VariantDefinition > ( ( VariantDefinition ) null ) ;
450441 }
451442
452- Debug . Assert ( featureDefinition . Variants != null ) ;
443+ Debug . Assert ( evaluationEvent . FeatureDefinition . Variants != null ) ;
444+
445+ evaluationEvent . VariantReason = "User Allocated" ;
453446
454447 return new ValueTask < VariantDefinition > (
455- featureDefinition
448+ evaluationEvent . FeatureDefinition
456449 . Variants
457450 . FirstOrDefault ( ( variant ) => variant . Name == user . Variant ) ) ;
458451 }
459452 }
460453 }
461454
462- if ( featureDefinition . Allocation . Group != null )
455+ if ( evaluationEvent . FeatureDefinition . Allocation . Group != null )
463456 {
464- foreach ( GroupAllocation group in featureDefinition . Allocation . Group )
457+ foreach ( GroupAllocation group in evaluationEvent . FeatureDefinition . Allocation . Group )
465458 {
466459 if ( TargetingEvaluator . IsTargeted ( targetingContext . Groups , group . Groups , _assignerOptions . IgnoreCase ) )
467460 {
468461 if ( string . IsNullOrEmpty ( group . Variant ) )
469462 {
470- _logger . LogWarning ( $ "Missing variant name for group allocation in feature { featureDefinition . Name } ") ;
463+ _logger . LogWarning ( $ "Missing variant name for group allocation in feature { evaluationEvent . FeatureDefinition . Name } ") ;
471464
472465 return new ValueTask < VariantDefinition > ( ( VariantDefinition ) null ) ;
473466 }
474467
475- Debug . Assert ( featureDefinition . Variants != null ) ;
468+ Debug . Assert ( evaluationEvent . FeatureDefinition . Variants != null ) ;
469+
470+ evaluationEvent . VariantReason = "Group Allocated" ;
476471
477472 return new ValueTask < VariantDefinition > (
478- featureDefinition
473+ evaluationEvent . FeatureDefinition
479474 . Variants
480475 . FirstOrDefault ( ( variant ) => variant . Name == group . Variant ) ) ;
481476 }
482477 }
483478 }
484479
485- if ( featureDefinition . Allocation . Percentile != null )
480+ if ( evaluationEvent . FeatureDefinition . Allocation . Percentile != null )
486481 {
487- foreach ( PercentileAllocation percentile in featureDefinition . Allocation . Percentile )
482+ foreach ( PercentileAllocation percentile in evaluationEvent . FeatureDefinition . Allocation . Percentile )
488483 {
489484 if ( TargetingEvaluator . IsTargeted (
490485 targetingContext ,
491486 percentile . From ,
492487 percentile . To ,
493488 _assignerOptions . IgnoreCase ,
494- featureDefinition . Allocation . Seed ?? $ "allocation\n { featureDefinition . Name } ") )
489+ evaluationEvent . FeatureDefinition . Allocation . Seed ?? $ "allocation\n { evaluationEvent . FeatureDefinition . Name } ") )
495490 {
496491 if ( string . IsNullOrEmpty ( percentile . Variant ) )
497492 {
498- _logger . LogWarning ( $ "Missing variant name for percentile allocation in feature { featureDefinition . Name } ") ;
493+ _logger . LogWarning ( $ "Missing variant name for percentile allocation in feature { evaluationEvent . FeatureDefinition . Name } ") ;
499494
500495 return new ValueTask < VariantDefinition > ( ( VariantDefinition ) null ) ;
501496 }
502497
503- Debug . Assert ( featureDefinition . Variants != null ) ;
498+ Debug . Assert ( evaluationEvent . FeatureDefinition . Variants != null ) ;
499+
500+ evaluationEvent . VariantReason = "Percentile Allocated" ;
504501
505502 return new ValueTask < VariantDefinition > (
506- featureDefinition
503+ evaluationEvent . FeatureDefinition
507504 . Variants
508505 . FirstOrDefault ( ( variant ) => variant . Name == percentile . Variant ) ) ;
509506 }
0 commit comments