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);{
"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.