Clywell.Core.FeatureFlags 1.0.0

dotnet add package Clywell.Core.FeatureFlags --version 1.0.0
                    
NuGet\Install-Package Clywell.Core.FeatureFlags -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Clywell.Core.FeatureFlags" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Clywell.Core.FeatureFlags" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Clywell.Core.FeatureFlags" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Clywell.Core.FeatureFlags --version 1.0.0
                    
#r "nuget: Clywell.Core.FeatureFlags, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Clywell.Core.FeatureFlags@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Clywell.Core.FeatureFlags&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Clywell.Core.FeatureFlags&version=1.0.0
                    
Install as a Cake Tool

Clywell.Core.FeatureFlags

Feature flag evaluation engine for .NET — rule-based rollout, tenant and user targeting, percentage rollouts, and pluggable providers. Zero infrastructure dependency for Application layer usage.

Clywell.Core.FeatureFlags Clywell.Core.FeatureFlags.AspNetCore License

Packages

Package Description
Clywell.Core.FeatureFlags Core evaluation engine. No ASP.NET Core dependency — safe for Application layer, workers, and background services.
Clywell.Core.FeatureFlags.AspNetCore HTTP enforcement primitives: MVC action filter attribute, request-level middleware gate, and Minimal API endpoint filter.

Overview

Clywell.Core.FeatureFlags is a lean, boolean-only feature flag engine with a fully pluggable provider model. It evaluates flags against rules and targeting conditions without bundled storage, caching, or UI. You bring the data source; the engine handles the evaluation.

Design principles:

  • Boolean only — flags are on or off; no strings, variants, or JSON payloads
  • Provider-owned caching — the engine resolves flags from your IFeatureFlagProvider on every evaluation; cache at the provider layer if needed
  • Zero infrastructureClywell.Core.FeatureFlags depends only on Microsoft.Extensions.DependencyInjection.Abstractions; use it in any .NET project
  • Composable conditions — combine built-in conditions with logical operators using a fluent API
  • Priority-ordered rules — the highest-priority matching rule wins; fall back to DefaultValue when no rule matches

Installation

Core package (required):

dotnet add package Clywell.Core.FeatureFlags

ASP.NET Core enforcement (optional):

dotnet add package Clywell.Core.FeatureFlags.AspNetCore

Table of Contents


Quick Start

1. Register services

// Core only (workers, background services, Application layer)
builder.Services.AddFeatureFlags();

// ASP.NET Core apps — registers both Core and ASP.NET Core primitives
builder.Services.AddFeatureFlagsAspNetCore();

// Always register your own provider
builder.Services.AddScoped<IFeatureFlagProvider, MyFeatureFlagProvider>();

2. Evaluate in application code

public class CheckoutHandler(IFeatureFlagService flags)
{
    public async Task Handle(CheckoutCommand cmd, CancellationToken ct)
    {
        // Empty context
        if (!await flags.IsEnabledAsync("new-checkout", ct))
            return;

        // Tenant + user context via builder delegate
        if (!await flags.IsEnabledAsync("new-checkout", ctx => ctx
                .WithTenant(cmd.TenantId)
                .WithUser(cmd.UserId), ct))
            return;

        // Evaluate multiple flags at once
        var results = await flags.EvaluateManyAsync(
            ["new-checkout", "express-shipping"],
            ctx => ctx.WithTenant(cmd.TenantId));
    }
}

3. Gate an MVC endpoint

[RequiresFeature("new-checkout")]
public IActionResult Checkout() => Ok();

4. Gate a Minimal API route

app.MapPost("/checkout", handler).RequireFeature("new-checkout");

Implementing a Provider

Implement IFeatureFlagProvider to connect the engine to your data source (database, config server, JSON file, etc.).

public class MyFeatureFlagProvider : IFeatureFlagProvider
{
    private readonly IFlagRepository _repo;

    public MyFeatureFlagProvider(IFlagRepository repo) => _repo = repo;

    public async Task<IEnumerable<FeatureFlag>> GetAllAsync(CancellationToken ct = default)
        => await _repo.GetAllFlagsAsync(ct);

    public async Task<FeatureFlag?> GetAsync(string key, CancellationToken ct = default)
        => await _repo.FindByKeyAsync(key, ct);
}

Caching is provider-owned. The engine calls your provider on every evaluation. Wrap a memory cache inside your provider if you want to avoid repeated database calls:

public async Task<FeatureFlag?> GetAsync(string key, CancellationToken ct = default)
    => await _cache.GetOrCreateAsync(key, entry =>
    {
        entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
        return _repo.FindByKeyAsync(key, ct);
    });

Evaluation Context

EvaluationContext carries the targeting data used by conditions during evaluation.

// Empty context — use when no targeting is required
var ctx = EvaluationContext.Empty;

// Build a context with the fluent builder
var ctx = new EvaluationContextBuilder()
    .WithTenant("tenant-abc")
    .WithUser("user-xyz")
    .WithAttribute("plan", "enterprise")
    .WithAttribute("region", "eu-west")
    .Build();

EvaluationContext is an immutable sealed record. Attributes is backed by a FrozenDictionary — zero-allocation lookups at evaluation time.


Evaluating Flags

IFeatureFlagService provides three overloads:

// 1. No context
bool on = await flags.IsEnabledAsync("flag-key", ct);

// 2. Pre-built context
var ctx = new EvaluationContextBuilder().WithTenant(tenantId).Build();
bool on = await flags.IsEnabledAsync("flag-key", ctx, ct);

// 3. Builder delegate (most common)
bool on = await flags.IsEnabledAsync("flag-key", ctx => ctx.WithTenant(tenantId), ct);

Convenience extensions for common cases:

// Tenant only
bool on = await flags.IsEnabledAsync("flag-key", tenantId, ct);

// Tenant + user
bool on = await flags.IsEnabledAsync("flag-key", tenantId, userId, ct);

// Evaluate multiple flags in one call
IReadOnlyDictionary<string, bool> results = await flags.EvaluateManyAsync(
    ["flag-a", "flag-b", "flag-c"],
    ctx => ctx.WithTenant(tenantId), ct);

Building Flags with FeatureFlagBuilder

Use FeatureFlagBuilder inside your IFeatureFlagProvider implementation to construct FeatureFlag instances with a fluent API.

FeatureFlag flag = FeatureFlagBuilder
    .For("new-checkout")
    .WithDescription("Gradual rollout of the redesigned checkout flow")
    .DisabledByDefault()
    .EnableWhen(new TenantCondition("tenant-early-access"))
    .EnableWhen(new AttributeCondition("plan", "enterprise"))
    .EnableWhen(new PercentageCondition("new-checkout", 10))
    .Build();

Rules added first receive the highest auto-assigned priority. Pass an explicit priority value to override:

.EnableWhen(new TenantCondition("tenant-vip"), priority: 100)
.EnableWhen(new PercentageCondition("new-checkout", 20), priority: 50)

Conditions Reference

All built-in conditions implement IEvaluationCondition.

Condition Match behaviour
AlwaysCondition Always true. Use as a lowest-priority catch-all to enable a feature for everyone.
TenantCondition(params string[]) true when context.TenantId is in the supplied set (case-insensitive).
UserCondition(params string[]) true when context.UserId is in the supplied set (case-insensitive).
AttributeCondition(key, value, comparison?) true when context.Attributes[key] equals value using the specified StringComparison (default: OrdinalIgnoreCase).
PercentageCondition(flagKey, percentage) true for a deterministic percentage% of the audience derived from a FNV-1a hash of flagKey + userId/tenantId. Same user/tenant always falls in the same bucket for the same flag.
AllOfCondition(conditions) Logical AND — true only when every inner condition matches.
AnyOfCondition(conditions) Logical OR — true when at least one inner condition matches.
NotCondition(inner) Logical NOT — inverts the result of the inner condition.
// TenantCondition — single or multiple tenants
new TenantCondition("tenant-a")
new TenantCondition("tenant-a", "tenant-b", "tenant-c")

// UserCondition — single or multiple users
new UserCondition("user-xyz")
new UserCondition("user-abc", "user-def")

// AttributeCondition — custom attribute matching
new AttributeCondition("plan", "enterprise")
new AttributeCondition("region", "eu-west", StringComparison.Ordinal)

// PercentageCondition — 10% deterministic rollout
new PercentageCondition("new-checkout", 10)

// AlwaysCondition — catch-all
AlwaysCondition.Instance

Composing Conditions

Use the Condition static factory or fluent extension methods to build complex targeting expressions.

Static factory

// All conditions must match (AND)
Condition.AllOf(
    new TenantCondition("tenant-a"),
    new AttributeCondition("plan", "enterprise"))

// At least one must match (OR)
Condition.AnyOf(
    new TenantCondition("tenant-a"),
    new UserCondition("user-xyz"))

// Invert a condition (NOT)
Condition.Not(new TenantCondition("tenant-blocked"))

Fluent extensions

// .And() — both must match
new TenantCondition("tenant-a")
    .And(new AttributeCondition("plan", "enterprise"))

// .Or() — either must match
new TenantCondition("tenant-a")
    .Or(new UserCondition("user-xyz"))

// .Negate() — invert
new TenantCondition("tenant-blocked").Negate()

Complex example

FeatureFlagBuilder
    .For("new-checkout")
    .DisabledByDefault()
    // Enable for enterprise tenants in the EU
    .EnableWhen(
        new TenantCondition("tenant-enterprise")
            .And(new AttributeCondition("region", "eu")))
    // OR: enable for beta users
    .EnableWhen(new UserCondition("user-beta-1", "user-beta-2"))
    // Block a specific tenant explicitly
    .DisableWhen(new TenantCondition("tenant-blocked"), priority: 1000)
    .Build();

ASP.NET Core Integration

Register ASP.NET Core primitives alongside the core engine:

builder.Services.AddFeatureFlagsAspNetCore(options =>
{
    options.DisabledStatusCode = StatusCodes.Status503ServiceUnavailable;
    // or redirect instead:
    // options.DisabledRedirectPath = "/maintenance";
});

// Always register your provider too
builder.Services.AddScoped<IFeatureFlagProvider, MyFeatureFlagProvider>();

AddFeatureFlagsAspNetCore automatically calls AddFeatureFlags — you do not need both calls.

Context note: All three HTTP enforcement primitives evaluate with EvaluationContext.Empty. They gate at the HTTP boundary before your application code resolves user/tenant identity. If you need tenant- or user-aware gating, use IFeatureFlagService directly inside your handler or middleware.


MVC Action Filter

[RequiresFeature] is an ActionFilterAttribute for MVC controllers and Razor Pages.

[RequiresFeature("new-checkout")]
public IActionResult Checkout() => Ok();

// Controller-level — gates all actions
[RequiresFeature("beta-dashboard")]
public class BetaController : Controller { ... }

Important: [RequiresFeature] only works on MVC controllers and Razor Pages. It is silently ignored on Minimal API endpoints. Use .RequireFeature() for Minimal APIs.


Minimal API Endpoint Filter

.RequireFeature() adds an IEndpointFilter to any IEndpointConventionBuilder — works with route groups too.

// Single endpoint
app.MapPost("/checkout", handler)
   .RequireFeature("new-checkout");

// Route group — gates all routes in the group
var beta = app.MapGroup("/beta")
              .RequireFeature("beta-features");

beta.MapGet("/dashboard", dashboardHandler);
beta.MapGet("/reports", reportsHandler);

Middleware Gate

UseFeatureGate blocks an entire path prefix when a flag is disabled.

// Return 503 when the flag is off
app.UseFeatureGate("maintenance-mode");

// Redirect to a specific path when the flag is off
app.UseFeatureGate("new-api", disabledPath: "/api/v1");

Options Reference

FeatureFlagOptions (core)

Property Default Description
DefaultValueWhenNotFound false Return value when the requested flag key is not found in the provider.
builder.Services.AddFeatureFlags(options =>
    options.WithDefaultValueWhenNotFound(false));

FeatureGateOptions (ASP.NET Core)

Property Default Description
DisabledStatusCode 404 HTTP status code returned when a gate blocks a request and no redirect is configured.
DisabledRedirectPath null When set, gates redirect to this path instead of returning DisabledStatusCode. Must be a relative or absolute URL.
builder.Services.AddFeatureFlagsAspNetCore(options =>
{
    options.DisabledStatusCode = StatusCodes.Status503ServiceUnavailable;
    options.DisabledRedirectPath = "/maintenance";
});

API Reference

IFeatureFlagService

Method Description
IsEnabledAsync(key, ct) Evaluate with empty context.
IsEnabledAsync(key, context, ct) Evaluate with a pre-built EvaluationContext.
IsEnabledAsync(key, configure, ct) Evaluate with a builder delegate.

FeatureFlagServiceExtensions

Method Description
IsEnabledAsync(key, tenantId, ct) Shortcut — evaluates with tenant context only.
IsEnabledAsync(key, tenantId, userId, ct) Shortcut — evaluates with tenant + user context.
EvaluateManyAsync(keys, context, ct) Evaluate multiple flags, returns IReadOnlyDictionary<string, bool>.
EvaluateManyAsync(keys, configure, ct) Same, with builder delegate.

IFeatureFlagProvider

Method Description
GetAllAsync(ct) Returns all flags. Used by evaluators that need the full set.
GetAsync(key, ct) Returns a single flag by key, or null if not found.

FeatureFlagBuilder

Method Description
For(key) Static factory — start building a flag with the given key.
WithDescription(desc) Set an optional human-readable description.
EnabledByDefault() Set DefaultValue = true.
DisabledByDefault() Set DefaultValue = false (default).
EnableWhen(condition, priority?) Add a rule that enables the flag when the condition matches.
DisableWhen(condition, priority?) Add a rule that disables the flag when the condition matches.
Build() Produce the immutable FeatureFlag instance.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Clywell.Core.FeatureFlags:

Package Downloads
Clywell.Core.FeatureFlags.AspNetCore

ASP.NET Core enforcement primitives for Clywell.Core.FeatureFlags — MVC action filter attribute, request-level middleware gate, and Minimal API endpoint filter. Requires Clywell.Core.FeatureFlags and an IFeatureFlagProvider registration.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 275 3/5/2026