Rules, feature flags, segments and A/B tests in .NET — from one expression language.
Write the condition once, as a string:
user.tier == 'gold' && cart.total > 100
Rulebook parses it, compiles it to a delegate, and evaluates it against whatever context you hand it. Feature flags, audience segments and A/B experiments are all the same rule with a different wrapper.
dotnet add package Rulebook
builder.Services.AddDecisions();
builder.Services.AddDecisionDefinitions(builder.Configuration);{
"Rulebook": {
"FeatureFlags": [
{
"Id": "new-checkout",
"Name": "New checkout",
"Rule": "user.tier == 'gold' && user.country == 'US'",
"RolloutPercentage": 25
}
]
}
}public sealed class CheckoutService(
IDecisionEngine engine,
IDecisionDefinitionProvider definitions)
{
public async Task<bool> UseNewCheckoutAsync(User user, CancellationToken cancellationToken)
{
var flags = await definitions.GetFeatureFlagsAsync(cancellationToken);
var flag = flags.Single(f => f.Id == "new-checkout");
var context = new DictionaryDecisionContext(new Dictionary<string, object?>
{
["user.tier"] = user.Tier,
["user.country"] = user.Country,
["subject.id"] = user.Id,
});
var result = await engine.EvaluateFeatureFlagAsync(flag, context, cancellationToken);
return result.IsMatch;
}
}Change the rule in configuration and the next evaluation uses it — no restart, no deploy. A rule that does not parse throws on first use and names the definition, rather than quietly evaluating to false.
| Rules | A small boolean DSL — == != > < >= <= in not in contains starts_with ends_with, &&, ||, !, parentheses. Parsed to an AST, compiled to a delegate, cached. |
| Feature flags | A rule plus a rollout percentage. |
| Segments | A named rule. Ask which segments a subject falls into. |
| A/B tests | Deterministic bucketing — XxHash32("{experimentId}:{subjectId}"), weighted variants, optional salt. The same subject always lands in the same variant, with no stored state and no coordination between instances. |
| Custom operators | Register your own; the registry is open. |
A decision is evaluated against an IDecisionContext, which resolves dotted paths. Nothing
about the shape of your data is baked in.
var context = new DictionaryDecisionContext(new Dictionary<string, object?>
{
["player.level"] = 25,
["player.country"] = "US",
["subject.id"] = "user-42",
});There is a synchronous fast path (TryGetValue) and an async fallback (GetValueAsync) for
values that have to be fetched. CompositeDecisionContext layers several sources.
AddDecisionDefinitions(IConfiguration) reads from configuration through IOptionsMonitor,
so any reloadable source gives hot reload. To load from a database or a feature-flag service
instead, implement IDecisionDefinitionProvider and register it:
services.AddDecisionDefinitionProvider<MyDefinitionProvider>();Registration is TryAdd, so yours wins over the built-in one regardless of order.
| Package | What you get |
|---|---|
Rulebook |
The engine: parser, compiler, operators, contexts, evaluators, definition sources |
Rulebook.Abstractions |
Contracts and definition models, with no dependencies. Reference this from a project that describes decisions without evaluating them. |
- Getting started
- Expressions — the rule language
- Context
- Feature flags
- Segmentation
- A/B testing
- Extensibility — custom operators and definition sources
- Architecture
- Release process
A runnable example lives in examples/Rulebook.Examples.Console,
and there are BenchmarkDotNet suites in tests/Rulebook.Benchmarks.
Semantic versioning. Both packages are versioned and released together. Breaking changes are listed in CHANGELOG.md.
Targets net10.0. Building from source needs the .NET 10 SDK.
Issues and pull requests are welcome — see CONTRIBUTING.md, CODE_OF_CONDUCT.md and SECURITY.md.
