GM.FeatureManagement 1.0.0

dotnet add package GM.FeatureManagement --version 1.0.0
                    
NuGet\Install-Package GM.FeatureManagement -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="GM.FeatureManagement" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GM.FeatureManagement" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.FeatureManagement" />
                    
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 GM.FeatureManagement --version 1.0.0
                    
#r "nuget: GM.FeatureManagement, 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 GM.FeatureManagement@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=GM.FeatureManagement&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.FeatureManagement&version=1.0.0
                    
Install as a Cake Tool

GM.FeatureManagement

Feature flags for the GM.* ecosystem — provider-agnostic, cache-backed, with targeting built for gradual rollouts and environment gating.

consumer ──▶ IFeatureManager ──▶ (GM.Caching, TTL) ──▶ IFeatureDefinitionProvider ──▶ flag source
             IsEnabled/GetVariant   fast, consistent      pluggable                     appsettings │ EF │ SaaS
Package Purpose Status
GM.FeatureManagement IFeatureManager, targeting, cache-backed evaluation, appsettings provider ✅ built
GM.FeatureManagement.AspNetCore [FeatureGate("Key")] + .RequireFeature("Key") for MVC / minimal-API endpoints ✅ built
GM.FeatureManagement.Mediator Pipeline behavior gating command/query handlers behind a flag ✅ built

Core concepts

  • IFeatureManager — the whole consumer surface:
    • IsEnabledAsync(key, context?) — is this feature on for this subject?
    • GetVariantAsync(key, context?) — which arm of an A/B test / rollout did this subject get?
  • FeatureContext — who/what you're evaluating for: UserId, TenantId, an explicit TargetingKey, and free-form Attributes. The TargetingId (key → user → tenant) is what percentage and variant assignment hash on, so a subject is sticky — same bucket every time, on every instance.
  • IFeatureDefinitionProviderwhere flags live, decoupled from evaluation. The default reads appsettings; swap in another implementation to move flags to a hosted service without changing consumer code (see Swapping the source).
  • FeatureDefinition — the declarative flag (below).

Evaluation order

IsEnabledAsync applies a definition's rules in a fixed order — first decisive rule wins:

  1. Master switch (Enabled: false) → off. A kill switch that beats everything.
  2. Environment gate (EnabledEnvironments) → off if the current env isn't listed (empty = all).
  3. Exclusions (ExcludedUsers / ExcludedTenants) → off.
  4. Inclusions (TargetUsers / TargetTenants) → on, bypassing the percentage gate.
  5. Percentage rollout (RolloutPercentage) → on only if the subject's stable bucket falls under the percentage. No TargetingId (anonymous) → conservatively off.
  6. Otherwise on — and GetVariantAsync assigns a weighted variant if any are declared.

Bucketing uses a stable SHA-256 hash, not string.GetHashCode() (which is randomised per process), so rollouts and variant assignments are identical across every replica and every restart.

Registration

Expects an ICacheService — call AddGMCaching() (in-memory) or, for cross-instance consistency, AddGMRedisCaching() first. Then, fluent:

builder.Services.AddGMCaching();

builder.Services.AddGMFeatureManagement(o =>
{
    o.CacheTtl = TimeSpan.FromSeconds(30); // change propagation window vs. store load

    // Gradual KYC vendor switch: shift 25% of users from Didit to Identomat.
    o.Features["KycVendorSwitch"] = new FeatureDefinition
    {
        Enabled = true,
        Variants =
        [
            new() { Name = "Didit",     Weight = 75 },
            new() { Name = "Identomat", Weight = 25, Configuration = { ["endpoint"] = "https://…" } },
        ],
    };

    // Environment-gate an in-progress feature to non-prod until it's stable.
    o.Features["NewPayments"] = new FeatureDefinition
    {
        Enabled = true,
        EnabledEnvironments = ["Development", "Staging"],
    };
});

…or from appsettings (FeatureManagement section binds the same shape):

builder.Services.AddGMCaching();
builder.Services.AddGMFeatureManagement(builder.Configuration.GetSection("FeatureManagement"));
"FeatureManagement": {
  "CacheTtl": "00:00:30",
  "Features": {
    "KycVendorSwitch": {
      "Enabled": true,
      "Variants": [ { "Name": "Didit", "Weight": 75 }, { "Name": "Identomat", "Weight": 25 } ]
    },
    "NewPayments": { "Enabled": true, "EnabledEnvironments": [ "Development", "Staging" ] }
  }
}

Using it

public class KycService(IFeatureManager features)
{
    public async Task<IKycVendor> ResolveVendorAsync(string userId)
    {
        // Sticky per user — a user assigned Identomat stays on Identomat as the rollout widens.
        var variant = await features.GetVariantAsync("KycVendorSwitch", FeatureContext.ForUser(userId));
        return variant.Name == "Identomat" ? _identomat : _didit;
    }

    public async Task<bool> PaymentsEnabledAsync(string userId, string tenantId) =>
        await features.IsEnabledAsync("NewPayments", FeatureContext.ForUser(userId, tenantId));
}

Why cache the definition (not the result)

Evaluation against a context is a cheap local hash; the potentially slow part is fetching the definition from the provider (trivial for appsettings, a network/DB hit for a hosted service). So the manager caches the definition in GM.Caching for CacheTtl — single-flight, so a burst of checks triggers one provider call, with negative caching for undefined keys. The result:

  • checks are fast and don't hammer the backing store,
  • a flag change propagates within the TTL — no redeploy,
  • with AddGMRedisCaching() the cache is shared, so all instances agree.

Swapping the flag source

IFeatureDefinitionProvider is the seam. The default is appsettings; register your own before AddGMFeatureManagement (the built-in uses TryAdd, so yours wins) to move flags to a hosted service or a database:

services.AddSingleton<IFeatureDefinitionProvider, LaunchDarklyFeatureProvider>();
// or an admin-UI store backed by GM.EntityFramework.Persistence
services.AddGMFeatureManagement(); // consumers still just inject IFeatureManager

Nothing on the consumer side changes — the cache, targeting, and evaluation are identical.

🚩 Flag — should GM.Secrets be the source of truth for flag values?

You asked me to flag rather than assume this. Recommendation: keep feature flags and secrets conceptually and operationally separate — do not make GM.Secrets the store for flag values.

Feature flags Secrets
Data Non-sensitive rollout config (percentages, env lists, targeting) Credentials, keys, connection strings
Change cadence Flip often, sometimes many times a day Rotate rarely
Audience Product / ops / an admin UI Tightly held, least-privilege
Blast radius of a mistake A feature bug A breach

Storing flags in a secrets store forces a bad trade: either you loosen access to the secret store so ops can toggle flags (weakening secret hygiene), or you lock flags behind secret-level access (so nobody can flip them quickly). They want opposite access models.

But they do meet at one point: a variant sometimes needs a credential — the Identomat arm of the KYC switch needs an Identomat API key. The clean pattern is a reference, not a copy: the flag's variant Configuration carries the secret's name (non-sensitive), and the consumer resolves the actual value through GM.Secrets at point of use.

"Variants": [
  { "Name": "Identomat", "Weight": 25, "Configuration": { "apiKeySecret": "kyc/identomat/api-key" } }
]
var variant = await features.GetVariantAsync("KycVendorSwitch", ctx);
var apiKey  = await secrets.GetAsync(variant["apiKeySecret"]!); // GM.Secrets resolves the value

So: flags point at secrets; they never store them. Two stores, one seam — not one store doing both.

Gating integrations

Each a thin package over IFeatureManager — install only what you use.

GM.FeatureManagement.AspNetCore

Gate endpoints; a closed gate returns a configurable status (404 by default, so an unreleased feature looks absent). The FeatureContext is built from the caller's user/tenant claims automatically.

builder.Services.AddGMFeatureManagementAspNetCore(); // claims-based context + the [FeatureGate] filter

// minimal API
app.MapGet("/payments/checkout", () => Results.Ok(...)).RequireFeature("NewPayments");

// MVC
[HttpGet, FeatureGate("NewPayments")]
public IActionResult Get() => Ok(...);

GM.FeatureManagement.Mediator

Gate a command/query handler — the handler never runs when the flag is off (a FeatureDisabledException short-circuits the pipeline):

builder.Services.AddGMFeatureGateBehavior(); // register in the GM.Mediator pipeline

// mark the request — a single-key marker…
public record RunPayoutCommand(string Account) : IRequest<Result>, IFeatureGatedRequest
{
    public string FeatureKey => "NewPayments";
}
// …or the attribute (supports multiple keys + Any/All)
[FeatureGate("NewPayments")]
public record RunPayoutCommand(string Account) : IRequest<Result>;

Both read the ambient FeatureContext from IFeatureContextAccessor — the AspNetCore package's claims-based accessor, or your own.

Conventions

Built to the shared GM.* conventions: net10.0, single lockstep <Version>, Conventional-Commits → Versionize releases, NuGet Trusted Publishing (OIDC), README + icon packed into every nupkg. Runnable usage lives in GM.FeatureManagement.Samples.

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 (2)

Showing the top 2 NuGet packages that depend on GM.FeatureManagement:

Package Downloads
GM.FeatureManagement.Mediator

GM.Mediator integration for GM.FeatureManagement: a pipeline behavior that gates command/query handlers behind a feature flag. Mark a request with IFeatureGatedRequest or [FeatureGate("Key")] and the handler runs only when the flag is enabled for the ambient FeatureContext; otherwise a FeatureDisabledException short-circuits the pipeline before the handler. Register with AddGMFeatureGateBehavior().

GM.FeatureManagement.AspNetCore

ASP.NET Core integration for GM.FeatureManagement: gate MVC controllers/actions and minimal-API endpoints behind feature flags. Annotate with [FeatureGate("Key")] or chain .RequireFeature("Key") on an endpoint; when the flag is off for the current request the endpoint returns a configurable status (404 by default), so an unreleased feature simply looks absent. The request's FeatureContext is built automatically from the user/tenant claims. Register with AddGMFeatureManagementAspNetCore().

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 109 8/7/2026