Skip to content

Defining Feature Flags

Feature flags are partial class types that implement IFeatureFlags<TConfig> and define flag properties as delegates. The source generator produces the constructor and Config property automatically.

Basic Structure

csharp
public partial class AppFeatureFlags : IFeatureFlags<AppSettings>
{
    // Required: when should these flags be cleaned up?
    public override DateTimeOffset ExpiresAt => new(2026, 6, 1, 0, 0, 0, TimeSpan.Zero);

    /// <summary>Enables the new onboarding flow.</summary>
    public FeatureFlag<bool> NewOnboarding => () => Config.EnableNewOnboarding;

    /// <summary>Maximum items shown in the new list view.</summary>
    public FeatureFlag<int> NewListViewMaxItems => () => Config.ListViewMax;
}

The class must be partial so the source generator can emit a constructor that accepts IReactiveConfig<AppSettings>. The generated Config property returns IReactiveConfig<T>.CurrentValue, so it always reflects the latest configuration.

Key Elements

ElementPurpose
IFeatureFlags<TConfig>Marks this as a feature flag class; source generator produces constructor and Config property
partial classRequired — the source generator emits the other half
ExpiresAtClass-level expiration date — when should these flags be removed?
ConfigSource-generated property — reads IReactiveConfig<TConfig>.CurrentValue
FeatureFlag<TResult>A no-context flag — returns a value based on current config
XML <summary>Description extracted by the source generator for health/REST endpoints

Flag Types

No-context flags

FeatureFlag<TResult> is a parameterless delegate. It reads from Config and returns a result:

csharp
/// <summary>Enables dark mode for all users.</summary>
public FeatureFlag<bool> DarkMode => () => Config.DarkModeEnabled;

Contextual flags

FeatureFlag<TContext, TResult> takes a context parameter — for decisions that depend on the current user, tenant, or request:

csharp
/// <summary>Gates the beta feature for specific users.</summary>
public FeatureFlag<UserContext, bool> BetaFeature => user => Config.BetaEnabled && user.IsBeta;

The TContext is resolved at evaluation time via a Context Resolver.

Multiple Config Sources ADV

A flag class can depend on multiple configuration types using a tuple:

csharp
public partial class RolloutFlags : IFeatureFlags<(FeatureConfig, TenantConfig)>
{
    public override DateTimeOffset ExpiresAt => new(2026, 9, 1, 0, 0, 0, TimeSpan.Zero);

    /// <summary>Enables new checkout when both feature and tenant allow it.</summary>
    public FeatureFlag<bool> NewCheckout => () =>
        Config.Item1.NewCheckoutEnabled &&
        Config.Item2.AllowExperiments;
}

The source generator injects IReactiveConfig<(FeatureConfig, TenantConfig)> and Config returns the combined tuple. When either config changes, the next flag invocation returns the updated result.

You can also use named tuple elements for readability:

csharp
public partial class RolloutFlags : IFeatureFlags<(FeatureConfig Features, TenantConfig Tenant)>
{
    public override DateTimeOffset ExpiresAt => new(2026, 9, 1, 0, 0, 0, TimeSpan.Zero);

    public FeatureFlag<bool> NewCheckout => () =>
        Config.Features.NewCheckoutEnabled &&
        Config.Tenant.AllowExperiments;
}

Return Types ADV

Flags can return any type, not just booleans:

csharp
/// <summary>Which checkout variant to show (A/B test).</summary>
public FeatureFlag<string> CheckoutVariant => () => Config.CheckoutVariant;

/// <summary>Rate limit for the new API (requests per minute).</summary>
public FeatureFlag<int> NewApiRateLimit => () => Config.NewApiRpm;

/// <summary>Full feature configuration for the experiment.</summary>
public FeatureFlag<ExperimentConfig> ExperimentSettings => () => Config.Experiment;

ExpiresAt

Every feature flag class must declare when its flags should be cleaned up:

csharp
public override DateTimeOffset ExpiresAt => new(2026, 6, 1, 0, 0, 0, TimeSpan.Zero);

This is a class-level expiration. A flag class groups flags belonging to one feature — the expiry date applies to the entire feature, not individual flags. When the feature is fully rolled out, the whole class should be removed.

After the expiry date:

  • Flags continue to work normally
  • The health API reports Degraded
  • This signals to the team that the feature rollout is complete and the flags should be cleaned up

The source generator validates that ExpiresAt returns a static value. Dynamic expressions are rejected at compile time.

Using Flags Directly

Inject the flag class and invoke properties directly:

csharp
public class CheckoutService(AppFeatureFlags flags)
{
    public async Task<CheckoutResult> ProcessAsync(Order order)
    {
        if (flags.NewCheckout())
            return await NewCheckoutFlow(order);

        return await LegacyCheckoutFlow(order);
    }
}

Flag classes are Singleton — safe to inject anywhere. The delegate reads from Config (backed by IReactiveConfig<T>.CurrentValue), which always reflects the latest configuration.

Released under the Apache-2.0 License.