Skip to content

How to Implement Hierarchical Feature Dependencies Without Circular DI? #579

@ShayMusachanov-dev

Description

@ShayMusachanov-dev

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

  1. Is this the recommended pattern for implementing hierarchical feature dependencies?
  2. Is there a built-in way to express feature dependencies without creating a custom filter?
  3. Is using IServiceProvider directly the correct approach to break the circular dependency, or is there a better pattern?
  4. 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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions