microsoft / microsoft/FeatureManagement-Dotnet

How to Implement Hierarchical Feature Dependencies Without Circular DI?

Open
#579 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
1.2k
Forks
129
Avg merge
1d 1h
Merged PRs (30d)
5

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

  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!

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the AddScopedFeatureManagement registration and the IFeatureManager/IFeatureFilter interaction shown in the issue. The payload names no repository files or tests; work is not bounded until maintainers clarify whether the desired outcome is a built-in dependency mechanism, guidance on deferred resolution, or cycle detection.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.