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
<PackageReference Include="Clywell.Core.FeatureFlags" Version="1.0.0" />
<PackageVersion Include="Clywell.Core.FeatureFlags" Version="1.0.0" />
<PackageReference Include="Clywell.Core.FeatureFlags" />
paket add Clywell.Core.FeatureFlags --version 1.0.0
#r "nuget: Clywell.Core.FeatureFlags, 1.0.0"
#:package Clywell.Core.FeatureFlags@1.0.0
#addin nuget:?package=Clywell.Core.FeatureFlags&version=1.0.0
#tool nuget:?package=Clywell.Core.FeatureFlags&version=1.0.0
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.
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
IFeatureFlagProvideron every evaluation; cache at the provider layer if needed - Zero infrastructure —
Clywell.Core.FeatureFlagsdepends only onMicrosoft.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
DefaultValuewhen 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
- Implementing a Provider
- Evaluation Context
- Evaluating Flags
- Building Flags with FeatureFlagBuilder
- Conditions Reference
- Composing Conditions
- ASP.NET Core Integration
- Options Reference
- API Reference
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, useIFeatureFlagServicedirectly 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 | Versions 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. |
-
net10.0
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 |