{"id":50834909,"url":"https://github.com/yanpitangui/komento","last_synced_at":"2026-06-14T02:31:49.458Z","repository":{"id":356182038,"uuid":"1225004377","full_name":"yanpitangui/komento","owner":"yanpitangui","description":"Komento is a high-performance .NET experimentation and feature flag engine built for deterministic evaluation, low latency, and pluggable architecture","archived":false,"fork":false,"pushed_at":"2026-06-10T22:48:30.000Z","size":206,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-10T23:06:10.732Z","etag":null,"topics":["ab-testing","csharp","dotnet","experimentation","feature-flags","feature-toggle","openfeature"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yanpitangui.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-29T21:10:28.000Z","updated_at":"2026-06-10T22:46:23.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yanpitangui/komento","commit_stats":null,"previous_names":["yanpitangui/komento"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/yanpitangui/komento","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yanpitangui%2Fkomento","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yanpitangui%2Fkomento/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yanpitangui%2Fkomento/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yanpitangui%2Fkomento/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yanpitangui","download_url":"https://codeload.github.com/yanpitangui/komento/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yanpitangui%2Fkomento/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34307683,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ab-testing","csharp","dotnet","experimentation","feature-flags","feature-toggle","openfeature"],"created_at":"2026-06-14T02:31:48.542Z","updated_at":"2026-06-14T02:31:49.452Z","avatar_url":"https://github.com/yanpitangui.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Komento\n\nA storage-agnostic experimentation and feature flag engine for .NET. Its primary operation is the *experiment check*: given a flag key and a subject, deterministically return which variant they belong to.\n\n```csharp\nvar result = await client.GetVariantAsync(\"checkout-flow\", userId, ctx);\nif (result == \"treatment\")\n    return NewCheckout();\n```\n\n**Design goals**\n\n- Zero I/O in the hot path — all evaluation is in-memory.\n- Near-zero allocations — `ValueTask`, `Span\u003cT\u003e`, `FrozenDictionary`, `ArrayPool\u003cT\u003e`.\n- Pluggable at every seam — swap in your own config source, segment store, subject resolver, or context enricher.\n- OpenFeature support via the `Komento.OpenFeature` provider package.\n\n---\n\n## Packages\n\n| Package | Purpose | NuGet |\n|---|---|---|\n| Komento | Core engine, all interfaces, DI registration | [![NuGet](https://img.shields.io/nuget/v/Komento.svg)](https://www.nuget.org/packages/Komento) |\n| Komento.AspNetCore | ASP.NET Core integration (filters, subject provider, enrichers) | [![NuGet](https://img.shields.io/nuget/v/Komento.AspNetCore.svg)](https://www.nuget.org/packages/Komento.AspNetCore) |\n| Komento.OpenFeature | OpenFeature provider adapter | [![NuGet](https://img.shields.io/nuget/v/Komento.OpenFeature.svg)](https://www.nuget.org/packages/Komento.OpenFeature) |\n---\n\n## Quick start\n\n### 1. Register Komento\n\n```csharp\nbuilder.Services\n    .AddKomento(options =\u003e\n    {\n        // Declare which experiments this service cares about.\n        options.Experiments = new HashSet\u003cstring\u003e { \"checkout-flow\", \"dark-mode\" };\n\n        // Attributes merged into every EvaluationContext at evaluation time.\n        options.StaticContext = EvaluationContext.Create()\n            .Set(\"region\", \"BR\")\n            .Set(\"service\", \"payments\")\n            .Build();\n    })\n    .AddSource\u003cAppSettingsExperimentSource\u003e(); // built-in: reads from appsettings.json\n```\n\n### 2. Load configs at startup\n\nCall `InitializeKomentoAsync` once before accepting traffic. It calls `IExperimentSource.LoadAsync` and feeds the result into the engine.\n\n```csharp\nawait app.Services.InitializeKomentoAsync();\nawait app.RunAsync();\n```\n\n### 3. Evaluate\n\nInject `IExperimentClient` anywhere:\n\n```csharp\npublic class CheckoutService(IExperimentClient experiments)\n{\n    public async Task\u003cIActionResult\u003e Checkout(string userId)\n    {\n        var ctx = EvaluationContext.Create().Set(\"platform\", \"web\").Build();\n        var variant = await experiments.GetVariantAsync(\"checkout-flow\", userId, ctx);\n\n        return variant == \"treatment\" ? NewFlow() : LegacyFlow();\n    }\n}\n```\n\nTyped helpers are available for simple flag cases:\n\n```csharp\nbool enabled = await experiments.GetBoolAsync(\"dark-mode\", userId, ctx);\nstring theme  = await experiments.GetStringAsync(\"ui-theme\", userId, ctx, defaultValue: \"default\");\n```\n\n### 4. Use Komento through OpenFeature\n\nIf your application already uses the OpenFeature .NET SDK, install `Komento.OpenFeature` and register `KomentoFeatureProvider` with the OpenFeature API.\n\n```csharp\nusing Komento.OpenFeature;\nusing OpenFeature;\nusing OpenFeature.Model;\n\nawait app.Services.InitializeKomentoAsync();\n\nvar experimentClient = app.Services.GetRequiredService\u003cIExperimentClient\u003e();\nApi.Instance.SetProvider(new KomentoFeatureProvider(experimentClient));\n\nvar client = Api.Instance.GetClient();\nvar ctx = EvaluationContext.Builder()\n    .SetTargetingKey(userId)\n    .Set(\"platform\", new Value(\"web\"))\n    .Build();\n\nbool enabled = await client.GetBooleanValueAsync(\"dark-mode\", false, ctx);\nstring theme = await client.GetStringValueAsync(\"ui-theme\", \"default\", ctx);\n```\n\n`KomentoFeatureProvider` maps OpenFeature requests onto `IExperimentClient`:\n\n| OpenFeature result | Komento behavior |\n|---|---|\n| `TARGETING_KEY_MISSING` | OpenFeature context has no `targetingKey` |\n| `FLAG_NOT_FOUND` | `IExperimentClient.ExperimentExists(flagKey)` is false |\n| `DEFAULT` | Subject is ineligible or an outsider |\n| `TARGETING_MATCH` | Subject was assigned a variant and the value type matched |\n| `PARSE_ERROR` | Variant value existed but could not be converted to the requested OpenFeature type |\n\n---\n\n## Configuration format (`appsettings.json`)\n\n```json\n{\n  \"Komento\": {\n    \"Experiments\": [\n      {\n        \"id\": \"checkout-flow\",\n        \"subjectType\": \"user\",\n        \"variants\": [\n          { \"name\": \"control\",   \"allocation\": 0.5 },\n          { \"name\": \"treatment\", \"allocation\": 0.5, \"value\": true }\n        ],\n        \"globalFilters\": [\n          { \"type\": \"trait-equals\",    \"key\": \"country\", \"value\": \"BR\" },\n          { \"type\": \"segment-include\", \"segment\": \"beta-users\" }\n        ],\n        \"overrides\": [\n          { \"type\": \"subject\", \"subjectId\": \"user-42\",       \"variant\": \"treatment\" },\n          { \"type\": \"segment\", \"segment\":  \"internal-staff\", \"variant\": \"treatment\" }\n        ]\n      }\n    ]\n  }\n}\n```\n\n**Allocation** is a `double` in `[0.0, 1.0]`. Allocations across all variants must sum to ≤ 1.0. Subjects whose hash falls outside all allocations are *outsiders* — they see control behavior but are excluded from experiment metrics.\n\n**Filter types**\n\n| `type` | Fields | Description |\n|---|---|---|\n| `trait-equals` | `key`, `value` | Subject must have `key = value` in `EvaluationContext` |\n| `segment-include` | `segment` | Subject must be a member of the named segment |\n\n**Override types**\n\n| `type` | Fields | Description |\n|---|---|---|\n| `subject` | `subjectId`, `variant` | Forces a specific subject into a variant, before bucket assignment |\n| `segment` | `segment`, `variant` | Forces all members of a segment into a variant, before bucket assignment |\n\n---\n\n## Assignment result\n\n`GetVariantAsync` always returns a `VariantResult`:\n\n```csharp\npublic readonly struct VariantResult\n{\n    public string  VariantName { get; init; }\n    public object? Value       { get; init; }  // optional typed payload from VariantConfig.Value\n    public bool    IsEligible  { get; init; }  // false when a global filter excluded the subject\n    public bool    IsOutsider  { get; init; }  // true when no variant bucket matched\n}\n```\n\nThe `==` operator compares against a variant name string directly:\n\n```csharp\nif (result == \"treatment\") { ... }\n```\n\n**Fallback table**\n\n| Situation | `VariantName` | `IsEligible` | `IsOutsider` |\n|---|---|---|---|\n| Experiment not found | `\"control\"` | `false` | `false` |\n| Subject failed a global filter | `\"control\"` | `false` | `false` |\n| Subject outside all buckets | `\"control\"` | `true` | `true` |\n| Normal assignment | variant name | `true` | `false` |\n\n---\n\n## Extension points\n\nKomento is designed to be extended rather than forked. The six interfaces below are the complete seam set.\n\n---\n\n### `IExperimentSource` — config loading\n\n```csharp\npublic interface IExperimentSource\n{\n    ValueTask\u003cIReadOnlyDictionary\u003cstring, ExperimentConfig\u003e\u003e LoadAsync(\n        IReadOnlySet\u003cstring\u003e experimentIds,\n        CancellationToken ct = default);\n}\n```\n\n**When to implement:** You store experiment definitions somewhere other than `appsettings.json` — a database, an HTTP API, a Redis key, a gRPC service. Implement `IExperimentSource` to pull the initial config at startup.\n\n`LoadAsync` is called once by `InitializeKomentoAsync`. It receives the set of experiment IDs declared in `KomentoOptions.Experiments` so you only fetch what this service cares about.\n\n**Built-in:** `AppSettingsExperimentSource` reads from `IConfiguration`. Good for local development and tests; not suitable for production systems where configs live in a database or a dedicated config service.\n\n**Registration:**\n\n```csharp\nbuilder.Services\n    .AddKomento(o =\u003e o.Experiments = [...])\n    .AddSource\u003cMyDatabaseExperimentSource\u003e();\n```\n\n**Example — HTTP source:**\n\n```csharp\npublic sealed class HttpExperimentSource(HttpClient http) : IExperimentSource\n{\n    public async ValueTask\u003cIReadOnlyDictionary\u003cstring, ExperimentConfig\u003e\u003e LoadAsync(\n        IReadOnlySet\u003cstring\u003e experimentIds, CancellationToken ct)\n    {\n        var response = await http.GetFromJsonAsync\u003cList\u003cExperimentConfig\u003e\u003e(\n            $\"/experiments?ids={string.Join(',', experimentIds)}\", ct);\n\n        return response?.ToDictionary(e =\u003e e.Id)\n               ?? (IReadOnlyDictionary\u003cstring, ExperimentConfig\u003e)new Dictionary\u003cstring, ExperimentConfig\u003e();\n    }\n}\n```\n\n---\n\n### `IConfigUpdater` — hot config reload\n\n```csharp\npublic interface IConfigUpdater\n{\n    IReadOnlySet\u003cstring\u003e RelevantExperimentIds { get; }\n\n    ValueTask UpdateAsync(IReadOnlyDictionary\u003cstring, ExperimentConfig\u003e configs, CancellationToken ct = default);\n    ValueTask UpdateAsync(ExperimentConfig config, CancellationToken ct = default);\n    ValueTask RemoveAsync(string experimentId, CancellationToken ct = default);\n}\n```\n\n**When to use:** Push config changes to the engine at runtime without restarting. The engine (`ExperimentClient`) implements this interface — inject it as `IConfigUpdater` to drive hot reloads from wherever change notifications arrive.\n\n`RelevantExperimentIds` returns the set declared in `KomentoOptions.Experiments`. External notifiers (message queues, Kafka consumers, WebSocket listeners) should filter against it so they don't process changes for experiments this service doesn't run.\n\nThe update is atomic: the engine builds a new `FrozenDictionary` and swaps the reference in one step. In-flight evaluations complete against the previous config; all subsequent calls see the new one.\n\n**Example — background polling service:**\n\n```csharp\npublic sealed class ExperimentPollingService(\n    IExperimentSource source,\n    IConfigUpdater updater,\n    ILogger\u003cExperimentPollingService\u003e log) : BackgroundService\n{\n    protected override async Task ExecuteAsync(CancellationToken ct)\n    {\n        while (!ct.IsCancellationRequested)\n        {\n            try\n            {\n                var configs = await source.LoadAsync(updater.RelevantExperimentIds, ct);\n                await updater.UpdateAsync(configs, ct);\n            }\n            catch (Exception ex) when (ex is not OperationCanceledException)\n            {\n                log.LogError(ex, \"Failed to refresh experiment configs\");\n            }\n\n            await Task.Delay(TimeSpan.FromSeconds(30), ct);\n        }\n    }\n}\n```\n\nRegister it alongside Komento:\n\n```csharp\nbuilder.Services.AddHostedService\u003cExperimentPollingService\u003e();\n```\n\n**Example — push notification (e.g., Kafka, Redis Pub/Sub):**\n\n```csharp\n// In your message consumer handler:\nif (updater.RelevantExperimentIds.Contains(incomingConfig.Id))\n    await updater.UpdateAsync(incomingConfig, ct);\n```\n\n---\n\n### `ISegmentProvider` — set membership\n\n```csharp\npublic interface ISegmentProvider\n{\n    ValueTask\u003cbool\u003e IsInSegmentAsync(string subjectId, string segmentName, CancellationToken ct = default);\n}\n```\n\n**When to implement:** You have large subject ID lists (allowlists, holdouts, beta cohorts) that need membership checks during filter or override evaluation. The engine calls this whenever a `SegmentIncludeFilter` or `SegmentOverride` appears in an experiment config.\n\nThis method is on the hot path. For static lists loaded at startup, implement it with binary search over a sorted in-memory array. For dynamic lists, a local cache with a short TTL is strongly recommended.\n\n**Built-in:** `InMemorySegmentProvider` — loaded at startup with a `Dictionary\u003cstring, IEnumerable\u003cstring\u003e\u003e`. Internally uses **BinSets**: sorted, deduplicated byte arrays where membership is a binary search, O(log n), allocation-free. Ideal for static lists that fit in RAM; millions of IDs are practical.\n\n**Registration:**\n\n```csharp\nbuilder.Services\n    .AddKomento(o =\u003e o.Experiments = [...])\n    .AddSegmentProvider\u003cMyRedisSegmentProvider\u003e();\n```\n\n**Example — Redis-backed provider with local cache:**\n\n```csharp\npublic sealed class RedisSegmentProvider(IDatabase redis) : ISegmentProvider\n{\n    private readonly ConcurrentDictionary\u003cstring, (DateTimeOffset Expires, bool Result)\u003e _cache = new();\n\n    public async ValueTask\u003cbool\u003e IsInSegmentAsync(string subjectId, string segmentName, CancellationToken ct)\n    {\n        var key = $\"{segmentName}:{subjectId}\";\n        if (_cache.TryGetValue(key, out var cached) \u0026\u0026 cached.Expires \u003e DateTimeOffset.UtcNow)\n            return cached.Result;\n\n        var isMember = await redis.SetContainsAsync(segmentName, subjectId);\n        _cache[key] = (DateTimeOffset.UtcNow.AddSeconds(30), isMember);\n        return isMember;\n    }\n}\n```\n\n---\n\n### `ISubjectProvider` *(Komento.AspNetCore)* — HTTP subject resolution\n\n```csharp\npublic interface ISubjectProvider\n{\n    string  SubjectType { get; }\n    string? GetSubject(HttpContext context);\n}\n```\n\n**When to implement:** You need the `[RequireVariant]` filter or `.RequireVariant()` endpoint filter to know *who* the current HTTP request is for. The integration matches providers to experiments by `SubjectType` — an experiment with `subjectType = \"user\"` is evaluated using the provider whose `SubjectType` is `\"user\"`.\n\nReturn `null` when no subject can be resolved (unauthenticated request, missing header, etc.). The filter treats a null subject as a miss and returns `404 Not Found`.\n\nRegister multiple providers to support different subject types:\n\n```csharp\nbuilder.Services\n    .AddKomentoAspNetCore()\n    .AddSubjectProvider\u003cUserSubjectProvider\u003e()    // SubjectType = \"user\"\n    .AddSubjectProvider\u003cTenantSubjectProvider\u003e(); // SubjectType = \"tenant\"\n```\n\n**Example — JWT claims provider:**\n\n```csharp\npublic sealed class UserSubjectProvider : ISubjectProvider\n{\n    public string SubjectType =\u003e \"user\";\n\n    public string? GetSubject(HttpContext context)\n        =\u003e context.User.FindFirstValue(ClaimTypes.NameIdentifier);\n}\n```\n\n**Example — API key header provider:**\n\n```csharp\npublic sealed class TenantSubjectProvider : ISubjectProvider\n{\n    public string SubjectType =\u003e \"tenant\";\n\n    public string? GetSubject(HttpContext context)\n        =\u003e context.Request.Headers[\"X-Tenant-Id\"].FirstOrDefault();\n}\n```\n\n---\n\n### `IEvaluationContextEnricher` *(Komento.AspNetCore)* — per-request context\n\n```csharp\npublic interface IEvaluationContextEnricher\n{\n    ValueTask EnrichAsync(HttpContext context, EvaluationContextBuilder builder, CancellationToken ct = default);\n}\n```\n\n**When to implement:** You need attributes beyond the static context (region, service name) to evaluate filters for HTTP requests — locale from the `Accept-Language` header, plan tier from JWT claims, country from a GeoIP lookup. Enrichers run in registration order before every evaluation triggered by `[RequireVariant]` or `.RequireVariant()`.\n\nEnrichers are synchronous-friendly — return `ValueTask.CompletedTask` if no async work is needed. If an enricher must call an external service, use `async`/`await` as normal.\n\n**Registration:**\n\n```csharp\nbuilder.Services\n    .AddKomentoAspNetCore()\n    .AddEnricher\u003cLocaleEnricher\u003e()\n    .AddEnricher\u003cClaimsEnricher\u003e();\n```\n\n**Example — locale from `Accept-Language`:**\n\n```csharp\npublic sealed class LocaleEnricher : IEvaluationContextEnricher\n{\n    public ValueTask EnrichAsync(HttpContext context, EvaluationContextBuilder builder, CancellationToken ct)\n    {\n        var locale = context.Request.Headers.AcceptLanguage.FirstOrDefault()?.Split(',')[0].Trim();\n        if (locale is not null)\n            builder.Set(\"locale\", locale);\n        return ValueTask.CompletedTask;\n    }\n}\n```\n\n**Example — plan tier from JWT claims:**\n\n```csharp\npublic sealed class ClaimsEnricher : IEvaluationContextEnricher\n{\n    public ValueTask EnrichAsync(HttpContext context, EvaluationContextBuilder builder, CancellationToken ct)\n    {\n        var plan = context.User.FindFirstValue(\"plan\");\n        if (plan is not null)\n            builder.Set(\"plan\", plan);\n        return ValueTask.CompletedTask;\n    }\n}\n```\n\n---\n\n### `IExperimentClient` — evaluation\n\n```csharp\npublic interface IExperimentClient\n{\n    ValueTask\u003cVariantResult\u003e GetVariantAsync(string flagKey, string subjectId, in EvaluationContext ctx, CancellationToken ct = default);\n\n    ValueTask\u003cbool\u003e   GetBoolAsync  (string flagKey, string subjectId, in EvaluationContext ctx, bool   defaultValue = default, CancellationToken ct = default);\n    ValueTask\u003cstring\u003e GetStringAsync(string flagKey, string subjectId, in EvaluationContext ctx, string defaultValue = \"\",      CancellationToken ct = default);\n    ValueTask\u003cint\u003e    GetIntAsync   (string flagKey, string subjectId, in EvaluationContext ctx, int    defaultValue = default, CancellationToken ct = default);\n    ValueTask\u003cdouble\u003e GetDoubleAsync(string flagKey, string subjectId, in EvaluationContext ctx, double defaultValue = default, CancellationToken ct = default);\n}\n```\n\n**When to implement:** Testing — mock or stub `IExperimentClient` to force variants in unit tests without running the real engine.\n\nThe concrete implementation (`ExperimentClient`) is registered as a singleton under both `IExperimentClient` and `IConfigUpdater`. It is internal; use the interfaces.\n\n**Example — test stub:**\n\n```csharp\npublic sealed class StubExperimentClient : IExperimentClient\n{\n    private readonly Dictionary\u003cstring, string\u003e _forced = new(StringComparer.Ordinal);\n\n    public StubExperimentClient Force(string flag, string variant)\n    {\n        _forced[flag] = variant;\n        return this;\n    }\n\n    public ValueTask\u003cVariantResult\u003e GetVariantAsync(\n        string flagKey, string subjectId, in EvaluationContext ctx, CancellationToken ct)\n    {\n        var name = _forced.GetValueOrDefault(flagKey, \"control\");\n        return ValueTask.FromResult(new VariantResult { VariantName = name, IsEligible = true });\n    }\n\n    public ValueTask\u003cbool\u003e   GetBoolAsync  (string f, string s, in EvaluationContext c, bool   d, CancellationToken ct) =\u003e ValueTask.FromResult(d);\n    public ValueTask\u003cstring\u003e GetStringAsync(string f, string s, in EvaluationContext c, string d, CancellationToken ct) =\u003e ValueTask.FromResult(d);\n    public ValueTask\u003cint\u003e    GetIntAsync   (string f, string s, in EvaluationContext c, int    d, CancellationToken ct) =\u003e ValueTask.FromResult(d);\n    public ValueTask\u003cdouble\u003e GetDoubleAsync(string f, string s, in EvaluationContext c, double d, CancellationToken ct) =\u003e ValueTask.FromResult(d);\n}\n```\n\n---\n\n## ASP.NET Core integration\n\n### Setup\n\n```csharp\nbuilder.Services\n    .AddKomento(o =\u003e { o.Experiments = [\"checkout-flow\"]; })\n    .AddSource\u003cAppSettingsExperimentSource\u003e();\n\nbuilder.Services\n    .AddKomentoAspNetCore()\n    .AddSubjectProvider\u003cUserSubjectProvider\u003e()\n    .AddEnricher\u003cLocaleEnricher\u003e();\n```\n\n### `[RequireVariant]` — MVC action filter\n\nGate a controller action on a variant assignment. Returns `404 Not Found` when the subject is not in the required variant.\n\n```csharp\n[HttpGet(\"new-checkout\")]\n[RequireVariant(\"checkout-flow\", \"treatment\")]\npublic IActionResult NewCheckout() =\u003e View();\n```\n\nApplied to an entire controller to gate all actions:\n\n```csharp\n[RequireVariant(\"admin-ui\", \"enabled\")]\n[ApiController, Route(\"admin\")]\npublic class AdminController : ControllerBase { ... }\n```\n\n### `.RequireVariant()` — minimal API endpoint filter\n\n```csharp\napp.MapGet(\"/new-checkout\", NewCheckoutHandler)\n   .RequireVariant(\"checkout-flow\", \"treatment\");\n```\n\n---\n\n## EvaluationContext\n\n`EvaluationContext` is an immutable snapshot of attributes used to evaluate filters. It is passed `in` everywhere — no struct copy on the hot path.\n\n```csharp\n// Build a context:\nvar ctx = EvaluationContext.Create()\n    .Set(\"platform\", \"android\")\n    .Set(\"country\", \"BR\")\n    .Build();\n\n// Extend an existing context (e.g., layering per-request data onto a base):\nvar extended = EvaluationContextBuilder.CreateFrom(baseCtx)\n    .Set(\"locale\", \"pt-BR\")\n    .Build();\n```\n\nAttributes set via `KomentoOptions.StaticContext` are merged automatically in `Komento.AspNetCore` before each evaluation. Request-level attributes from enrichers layer on top.\n\n---\n\n## Performance\n\nAll read operations — variant lookup, filter evaluation, segment membership — are allocation-free on the hot path. BenchmarkDotNet (`MemoryDiagnoser`) shows **0 bytes allocated** per `GetVariantAsync` call across all paths including the async segment filter path.\n\nKey mechanisms:\n\n- `FrozenDictionary\u003cstring, CompiledExperiment\u003e` — experiment map, lock-free reads.\n- `ArrayPool\u003cbyte\u003e` — XxHash64 input buffer, no heap allocation per hash.\n- `ValueTask\u003cT\u003e` — synchronous results wrapped without `Task` allocation.\n- `in EvaluationContext` — struct passed by reference, never copied.\n- No LINQ, no closures in any hot-path method.\n\nConfig updates (`UpdateAsync`) are the only write operation. They build a new `FrozenDictionary` on a background thread and atomically swap the reference — in-flight reads are unaffected.\n\n---\n\n## Development\n\n```bash\n# Build\ndotnet build\n\n# Tests\ndotnet test\n\n# Single test\ndotnet test --filter \"FullyQualifiedName~Unknown_experiment_returns_NotFound\"\n\n# Benchmarks (Release mode required)\ndotnet run --project benchmarks/Komento.Benchmarks/ -c Release\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyanpitangui%2Fkomento","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyanpitangui%2Fkomento","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyanpitangui%2Fkomento/lists"}