Skip to content
Back to skills

Dotnet Feature Flags

ASecurity

Reviews ASP.NET Core feature-flag usage via Microsoft.FeatureManagement — the rollout-specific extension of dotnet-dynamic-configuration's generic DB-backed settings model. Flags feature branching done with ad-hoc if/config checks instead of IFeatureManager, no targeting-filter support for percentage rollout or user/tenant allow-lists, flags left in code long after a rollout completed, and flag evaluation results not exposed to the Angular frontend consistently. Outputs findings with pilot-do...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 6, 2026
developmenttypescriptgoangularapifrontendbackend

Works with

  • api

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill dotnet-feature-flags --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dotnet Feature Flags?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dotnet Feature Flags
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-dotnet-feature-flags/badge)](https://www.skillsdirectory.com/skills/agenticpawan-dotnet-feature-flags)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: dotnet-feature-flags
description: Reviews ASP.NET Core feature-flag usage via Microsoft.FeatureManagement — the rollout-specific extension of dotnet-dynamic-configuration's generic DB-backed settings model. Flags feature branching done with ad-hoc if/config checks instead of IFeatureManager, no targeting-filter support for percentage rollout or user/tenant allow-lists, flags left in code long after a rollout completed, and flag evaluation results not exposed to the Angular frontend consistently. Outputs findings with pilot-dotnet feature-flags standard IDs.
when_to_use: feature flag, IFeatureManager, Microsoft.FeatureManagement, percentage rollout, targeting filter, feature gate, stale feature flag, flag cleanup, canary release, A/B rollout, beta feature toggle
---

## Standard IDs

| ID | Severity | What it checks |
|----|----------|-----------------|
| FF-001 | P1 | Feature branching done with ad-hoc `if(config[...])` instead of `IFeatureManager` |
| FF-002 | P1 | No targeting-filter support for percentage rollout or user/tenant allow-lists |
| FF-003 | P2 | Feature flag left in code long after its rollout completed |
| FF-004 | P2 | Flag evaluation not exposed to the Angular frontend consistently |

Builds on `dotnet-dynamic-configuration`'s DB-backed settings model — a feature flag is a
specific *kind* of dynamic configuration value with its own evaluation semantics
(percentage/targeting), not just an on/off boolean read from the settings table.

---

## Check A — Ad-hoc config checks instead of IFeatureManager (FF-001)

### Detection

Grep for `_configuration["Features:X"] == "true"` or a hand-rolled boolean read used to
gate a code path, instead of `IFeatureManager.IsEnabledAsync(...)`. The hand-rolled
version has no targeting/percentage support and no consistent evaluation contract across
the codebase — every feature check reinvents its own on/off logic.

### BAD — ad-hoc config-driven branching

```csharp
public async Task<IActionResult> Checkout(CheckoutDto dto)
{
    if (_configuration["Features:NewCheckoutFlow"] == "true") // string comparison, no targeting
    {
        return await _newCheckoutService.ProcessAsync(dto);
    }
    return await _legacyCheckoutService.ProcessAsync(dto);
}
```

### GOOD — IFeatureManager, backed by the DB-backed configuration source

```csharp
builder.Services.AddFeatureManagement()
    .AddFeatureFilter<PercentageFilter>()
    .AddFeatureFilter<TargetingFilter>();

public async Task<IActionResult> Checkout(CheckoutDto dto, IFeatureManager featureManager)
{
    if (await featureManager.IsEnabledAsync("NewCheckoutFlow"))
        return await _newCheckoutService.ProcessAsync(dto);
    return await _legacyCheckoutService.ProcessAsync(dto);
}
```

---

## Check B — No targeting-filter support (FF-002)

### Detection

For a flag intended to roll out gradually (not just be a static on/off switch), check
whether `TargetingFilter`/`PercentageFilter` is actually configured with targeting
criteria (specific users, tenants, or a percentage of traffic), versus a flag that's
either 0% or 100% enabled everywhere with no gradual-rollout mechanism.

### BAD — flag is a blunt global switch with no rollout control

```json
{
  "FeatureManagement": {
    "NewCheckoutFlow": true
  }
}
```

### GOOD — percentage rollout + beta-tenant allow-list

```json
{
  "FeatureManagement": {
    "NewCheckoutFlow": {
      "EnabledFor": [
        { "Name": "Percentage", "Parameters": { "Value": 10 } },
        { "Name": "Targeting", "Parameters": { "Audience": {
          "Groups": [{ "Name": "BetaTenants", "RolloutPercentage": 100 }],
          "DefaultRolloutPercentage": 0
        }}}
      ]
    }
  }
}
```

```csharp
public class TenantTargetingContextAccessor : ITargetingContextAccessor
{
    private readonly ICurrentUserService _currentUser;

    public ValueTask<TargetingContext> GetContextAsync() =>
        new(new TargetingContext
        {
            UserId = _currentUser.UserId.ToString(),
            Groups = new[] { _currentUser.TenantGroup }
        });
}
```

---

## Check C — Stale flag never cleaned up (FF-003)

### Detection

Check whether a flag that has been at 100% rollout (or 0%, permanently killed) for an
extended period still has both code branches present, plus the flag definition itself.
Every stale flag is dead-code risk and cognitive overhead — the "old" branch usually
stops being tested once the rollout completes but stays in the codebase indefinitely.

### BAD — flag fully rolled out months ago, both branches still exist

```csharp
// NewCheckoutFlow has been at 100% for 6 months; legacy path is untested dead code
// that still gets *compiled* and *reviewed* in every unrelated PR that touches this file.
if (await featureManager.IsEnabledAsync("NewCheckoutFlow"))
    return await _newCheckoutService.ProcessAsync(dto);
return await _legacyCheckoutService.ProcessAsync(dto);
```

### GOOD — flag removed once rollout is confirmed complete, only the winning path remains

```csharp
// NewCheckoutFlow flag removed 2026-01-15 after 100% rollout confirmed stable for 30 days.
return await _newCheckoutService.ProcessAsync(dto);
```

Track flag age/rollout percentage in the same DB-backed settings admin surface
`dotnet-dynamic-configuration` CFG-005 recommends, and flag any feature at 100%/0% for
longer than a documented threshold (e.g., 60 days) for cleanup.

---

## Check D — Flag evaluation not exposed to the frontend consistently (FF-004)

### Detection

For a flag that also changes Angular UI behavior (not just backend logic), check whether
the frontend re-implements its own copy of the same targeting logic instead of querying a
single `/api/features` endpoint that evaluates via the same `IFeatureManager` the backend
uses — two independent evaluations of "is this user in the 10% rollout" can disagree.

### BAD — Angular re-derives its own flag logic

```typescript
// Angular guesses at rollout eligibility independently of the backend's IFeatureManager
const isBeta = localStorage.getItem('betaUser') === 'true';
```

### GOOD — one evaluation source, exposed via an endpoint

```csharp
[HttpGet("api/features")]
public async Task<IActionResult> GetFeatures(IFeatureManager featureManager)
{
    var flags = new Dictionary<string, bool>();
    await foreach (var name in featureManager.GetFeatureNamesAsync())
        flags[name] = await featureManager.IsEnabledAsync(name);
    return Ok(flags);
}
```

```typescript
// FeatureFlagService fetches once per session from the same evaluation the backend uses
this.flags = await this.http.get<Record<string, boolean>>('/api/features').toPromise();
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…