GM.FeatureManagement.Mediator 1.0.0

dotnet add package GM.FeatureManagement.Mediator --version 1.0.0
                    
NuGet\Install-Package GM.FeatureManagement.Mediator -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.Mediator" 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.Mediator" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.FeatureManagement.Mediator" />
                    
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.Mediator --version 1.0.0
                    
#r "nuget: GM.FeatureManagement.Mediator, 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.Mediator@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.Mediator&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.FeatureManagement.Mediator&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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 92 8/7/2026