-
Notifications
You must be signed in to change notification settings - Fork 123
Description
Context
I'm implementing hierarchical feature dependencies where one feature can depend on other features being enabled. I'm using a database-backed provider (similar to PR #389) with AddScopedFeatureManagement().
The Scenario
I want to implement a FeatureDependency filter that allows one feature to depend on others:
{
"id": "Dashboard",
"enabled": true,
"conditions": {
"requirement_type": "Any",
"client_filters": [
{
"name": "FeatureDependency",
"parameters": { "FeatureName": "BasicReporting" }
},
{
"name": "FeatureDependency",
"parameters": { "FeatureName": "AdvancedReporting" }
}
]
}
}The Dashboard feature should be enabled if either BasicReporting OR AdvancedReporting is enabled.
These dependent features themselves have their own conditions:
{
"id": "BasicReporting",
"enabled": true,
"conditions": {
"client_filters": [
{
"name": "TimeWindow",
"parameters": { "Start": "...", "End": "..." }
}
]
}
}{
"id": "AdvancedReporting",
"enabled": true,
"conditions": {
"requirement_type": "All",
"client_filters": [
{
"name": "Percentage",
"parameters": { "Value": 50 }
},
{
"name": "FeatureDependency",
"parameters": { "FeatureName": "PremiumSubscription" }
}
]
}
}To properly evaluate Dashboard, the FeatureDependency filter needs to evaluate BasicReporting and AdvancedReporting through their complete filter pipeline (TimeWindow, Percentage, nested dependencies, etc.). This requires calling IFeatureManager.IsEnabledAsync().
What I Tried
I implemented the filter like this:
[FilterAlias("FeatureDependency")]
public class FeatureDependencyFilter : IFeatureFilter
{
private readonly IFeatureManager _featureManager;
public FeatureDependencyFilter(IFeatureManager featureManager)
{
_featureManager = featureManager;
}
public async Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
string? featureName = context.Parameters.GetValue<string>("FeatureName");
if (string.IsNullOrEmpty(featureName))
{
return false;
}
return await _featureManager.IsEnabledAsync(featureName);
}
}Registered with:
services.AddScopedFeatureManagement()
.AddFeatureFilter<FeatureDependencyFilter>();The Problem
This creates a circular dependency during DI container construction:
IFeatureManager (being created)
→ needs IFeatureFilterMetadata[] (including FeatureDependencyFilter)
→ FeatureDependencyFilter constructor needs IFeatureManager
→ Circular dependency!
Result: The application hangs indefinitely when trying to resolve IFeatureManager:
using (IServiceScope scope = serviceProvider.CreateScope())
{
IFeatureManager featureManager = scope.ServiceProvider.GetRequiredService<IFeatureManager>();
}The Solution I Arrived At
Breaking the circular dependency by resolving IFeatureManager at evaluation time instead of in the constructor:
[FilterAlias("FeatureDependency")]
public class FeatureDependencyFilter : IFeatureFilter
{
private readonly IServiceProvider _serviceProvider;
public FeatureDependencyFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
{
string? featureName = context.Parameters.GetValue<string>("FeatureName");
if (string.IsNullOrEmpty(featureName))
{
return false;
}
IFeatureManager featureManager = _serviceProvider.GetRequiredService<IFeatureManager>();
return await featureManager.IsEnabledAsync(featureName);
}
}Questions
- Is this the recommended pattern for implementing hierarchical feature dependencies?
- Is there a built-in way to express feature dependencies without creating a custom filter?
- Is using
IServiceProviderdirectly the correct approach to break the circular dependency, or is there a better pattern? - Does the default configuration-based provider handle cycle detection? If feature dependencies form a cycle (e.g., FeatureA → FeatureB → FeatureA), does the library have built-in safeguards, or should I implement my own cycle detection?
Any guidance on the proper pattern for hierarchical feature dependencies would be greatly appreciated!