Elarion

Feature flag providers

Declare feature flags in modules, then give backend-evaluated flags an owner by wiring a feature-flag backend into the host.

Every feature flag is declared once and has exactly one owner: a resolver class in its module ([FeatureFlag], code-defined) or the host's backend ([BackendFeatureFlag], evaluated by Microsoft.FeatureManagement or an OpenFeature provider). The attributes, the evaluation contracts and IFeatureFlagService ship in Elarion.Abstractions/Elarion core with no feature-management dependency; this page covers the host wiring for backend-evaluated flags. For declarations, gates, the evaluation context and the compile-time checks see Feature flags & variants.

Code-defined flags need no package

A class that implements IFeatureFlagResolver and carries [FeatureFlag] is wired by the module generator — nothing to register in the host:

[FeatureFlag("beta-reports", ExposeToClient = true)]
public sealed class BetaReportsFlag(IBetaRoster roster) : IFeatureFlagResolver { /* ... */ }

Choose a backend for backend-evaluated flags

A host that declares [BackendFeatureFlag]s must register exactly one IBackendFeatureFlagEvaluator, otherwise it fails at startup. Two opt-in packages provide one, built on OpenFeature; the backend answers only for the flags declared as backend-evaluated, so switching one for the other never touches a [FeatureGate].

Batteries-included: FeatureManagement

Elarion.FeatureFlags.FeatureManagement wires the OpenFeature Microsoft.FeatureManagement provider so declared flags read config-driven values out of the box. Best for getting started.

Bring your own provider: OpenFeature

Elarion.FeatureFlags.OpenFeature registers the evaluator over any OpenFeature provider you configure (LaunchDarkly, ConfigCat, flagd, Flagsmith, ...). Best when a flag platform is already in play.

Config-driven flags (FeatureManagement)

builder.Services.AddElarionFeatureManagement(builder.Configuration);
appsettings.json
{
  "FeatureManagement": {
    "new-export": true,
    "new-dashboard": false
  }
}

with the names declared in a module:

[AppModule("Billing")]
[BackendFeatureFlag("new-export")]
[BackendFeatureFlag("new-dashboard")]
public static class BillingModule { }

Any OpenFeature provider

builder.Services.AddOpenFeature(b => b.AddProvider(/* LaunchDarkly, ConfigCat, flagd, ... */));
builder.Services.AddElarionOpenFeature();

AddElarionFeatureManagement is sugar over AddOpenFeature(...) + AddElarionOpenFeature(). To replace the backend wholesale (a custom store, a test double), register your own IBackendFeatureFlagEvaluator.

Targeting comes from the explicit FeatureEvaluationContext: the OpenFeature context is built from its user id (targeting key, plus UserId/Groups), tenant id (TenantId) and custom attributes. For the ambient request that context is derived from ICurrentUser and the tenant scope, so rollouts work off-HTTP the same as on, with no HttpContext.

Gate a handler

Once a backend is wired, annotate the handler. The FeatureGateDecorator the generator inserts evaluates the flag just inside the authorization gate, before caching, validation, and the handler body:

[Handler("billing.export")]
[FeatureGate("new-export")]
public sealed class ExportInvoices(AppDbContext db)
    : IHandler<ExportInvoices.Command, Result<ExportInvoices.Response>> { /* ... */ }

A closed gate short-circuits to AppError.NotFound (404), identically under JSON-RPC, MCP, and HTTP — a gated-off operation is indistinguishable from one that doesn't exist. See the concept page for the All/Any requirement forms, Negate, and the ELFEAT001/ELFEAT002 diagnostics.

Swap an implementation per variant

Beyond on/off gating, a flag can allocate a variant per user and resolve a different [Service] implementation for it. Mark each implementation of a contract with [FeatureVariant] (a modifier on [Service]); consumers inject the plain contract and stay unaware:

[Service]
[FeatureVariant("ForecastAlgorithm")]                 // the default (no Variant)
public sealed class LinearForecast : IForecastAlgorithm { /* ... */ }

[Service]
[FeatureVariant("ForecastAlgorithm", Variant = "neural")]
public sealed class NeuralForecast : IForecastAlgorithm { /* ... */ }

The generator wires variant resolution automatically; outside a handler constructor, inject IVariantServiceProvider<IForecastAlgorithm>. See Variant service injection for how the async-resolving proxy keeps contracts synchronous, and the provider requirement for surfacing variant names.

For a process-global, admin- or ops-made choice (which e-mail backend?), use the configuration-selected sibling [ConfigurationVariant("Email:Backend", Value = "office365")] instead: a plain IConfiguration value picks the implementation synchronously — no flag provider, no proxy — and pairing it with the settings bridge makes the switch admin-writable at run time, cluster-wide. The generated ElarionVariants registry catalogs every switch (keys, value consts for [AllowedValues], descriptors) so admin surfaces enumerate and validate with zero hand-kept lists — seed it with AddElarionVariantCatalog(ElarionVariants.All) + AddElarionVariantValidation(). Choose the axis by who decides: per-caller experiments are [FeatureVariant]; a process-global admin/ops choice is [ConfigurationVariant] — and when in doubt, start configuration-selected (the smaller machine). For switchable adapters the recommended shape is a port-owned vocabulary (consts beside the port). See Feature-selected or configuration-selected? and Who owns the switch.

On this page