Examples & Recipes¶
Complete, runnable scenarios. Every snippet uses only public API; expected results are shown in comments.
1. Request authorization policy¶
The classic CEL use case: gate an action on attributes of the request.
using Celly;
using Celly.Checking;
using Celly.Types;
using Celly.Values;
var env = CelEnv.Create(new CelEnvSettings
{
Declarations =
[
new VariableDecl("request", CelType.Map(CelType.String, CelType.Dyn)),
new VariableDecl("resource", CelType.Map(CelType.String, CelType.Dyn)),
],
});
// Validate the rule ONCE at admin time (parse + check), store it, reuse it.
const string rule = """
request.user.role in ['admin', 'editor'] &&
resource.owner == request.user.name ||
request.user.role == 'superadmin'
""";
var parsed = env.Parse(rule);
if (parsed.Ast is null || env.Check(parsed.Ast).HasErrors)
{
throw new ArgumentException("invalid policy rule");
}
var program = env.Program(parsed.Ast); // compile once…
// …evaluate per request (thread-safe, allocation-light):
bool Allowed(string name, string role, string owner)
{
var result = program.Eval(new Dictionary<string, object?>
{
["request"] = new Dictionary<string, object?>
{
["user"] = new Dictionary<string, object?> { ["name"] = name, ["role"] = role },
},
["resource"] = new Dictionary<string, object?> { ["owner"] = owner },
});
return result is BoolValue { Value: true }; // errors fail closed
}
Allowed("amy", "editor", "amy"); // true (role ok + owns the resource)
Allowed("amy", "editor", "bob"); // false (not the owner)
Allowed("sam", "superadmin", "bob"); // true (superadmin bypass)
2. Feature-flag targeting¶
var env = CelEnv.Create(new CelEnvSettings
{
Declarations =
[
new VariableDecl("user", CelType.Map(CelType.String, CelType.Dyn)),
new VariableDecl("now", CelType.Timestamp),
],
});
var flag = env.Compile("""
user.plan == 'enterprise' ||
user.email.endsWith('@example.com') ||
(user.beta_opt_in && now < timestamp('2026-09-01T00:00:00Z'))
""");
var enabled = flag.Eval(new Dictionary<string, object?>
{
["user"] = new Dictionary<string, object?>
{
["plan"] = "free", ["email"] = "[email protected]", ["beta_opt_in"] = false,
},
["now"] = Celly.Values.TimestampValue.Of(DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 0),
});
// BoolValue.True — matched the email domain clause
3. Input validation (protovalidate style)¶
One rule per field; collect violations instead of stopping at the first.
var env = CelEnv.Create(new CelEnvSettings
{
Declarations = [new VariableDecl("this", CelType.Map(CelType.String, CelType.Dyn))],
Libraries = [Celly.Extensions.StringsLibrary.Instance],
});
var rules = new (string Field, string Rule, string Message)[]
{
("email", "this.email.matches('^[^@]+@[^@]+$')", "must be a valid email"),
("age", "this.age >= 13 && this.age < 130", "must be between 13 and 129"),
("username", "this.username.size() >= 3 && this.username.matches('^[a-z0-9_]+$')",
"3+ chars, lowercase alphanumeric"),
("tags", "this.tags.all(t, t.size() <= 20) && size(this.tags) <= 5",
"at most 5 tags of at most 20 chars"),
};
var programs = rules.Select(r => (r.Field, r.Message, Program: env.Compile(r.Rule))).ToList();
var subject = new Dictionary<string, object?>
{
["this"] = new Dictionary<string, object?>
{
["email"] = "not-an-email",
["age"] = 25L,
["username"] = "amy_9",
["tags"] = new[] { "a", "way-too-long-tag-name-over-twenty-chars" },
},
};
var violations = programs
.Where(p => p.Program.Eval(subject) is not BoolValue { Value: true })
.Select(p => $"{p.Field}: {p.Message}")
.ToList();
// ["email: must be a valid email", "tags: at most 5 tags of at most 20 chars"]
4. Comprehension cookbook¶
// Filtering + shaping
orders.filter(o, o.status == 'open').map(o, o.id) // ids of open orders
prices.filter(p, p > 0).size() == prices.size() // all positive?
items.exists(i, i.sku == wanted) // containment by field
users.exists_one(u, u.is_primary) // exactly one primary
// Maps iterate over KEYS
{'a': 1, 'b': 2}.all(k, k in allowed_keys)
labels.filter(k, labels[k] == 'prod') // keys whose value matches
// Two-variable forms (TwoVarComprehensionsLibrary)
scores.all(i, v, v >= scores[0]) // list: index, value
labels.transformMap(k, v, v.lowerAscii()) // map over values
inventory.transformMapEntry(sku, count, {count: sku}) // invert a map
// Aggregation via chaining (no fold operator needed for common cases)
size(items.filter(i, i.qty > 0)) // count matching
items.map(i, i.price).exists(p, p > limit) // any over projection
// Nested data
teams.all(t, t.members.exists(m, m.role == 'lead'))
matrix.map(row, row.map(cell, cell * 2)) // nested map
5. Timestamps: expiry, windows, business hours¶
// Token expiry
now < token.issued_at + duration('15m')
// Within a date window
now >= campaign.start && now < campaign.end
// Business hours in a specific timezone (getHours is tz-aware)
now.getHours('America/New_York') >= 9 && now.getHours('America/New_York') < 17 &&
now.getDayOfWeek('America/New_York') in [1, 2, 3, 4, 5] // Mon..Fri (0 = Sunday)
// Age in days
(now - user.created_at) > duration('720h') // older than 30 days
// Binding 'now' from .NET:
["now"] = TimestampValue.Of(DateTimeOffset.UtcNow.ToUnixTimeSeconds(), 0)
6. Optionals: tolerant reads over ragged data¶
var env = CelEnv.Create(new CelEnvSettings
{
Declarations = [new VariableDecl("cfg", CelType.Map(CelType.String, CelType.Dyn))],
Libraries = [Celly.Extensions.OptionalsLibrary.Instance],
});
// Missing anywhere along the chain → the default, never an error:
var timeout = env.Compile("cfg.?server.?timeout_ms.orValue(5000)");
timeout.Eval(new Dictionary<string, object?> { ["cfg"] = new Dictionary<string, object?>() });
// IntValue 5000
// First present wins:
env.Compile("cfg.?override.or(cfg.?fallback).orValue('none')");
// Optional entries: include keys conditionally
env.Compile("{'always': 1, ?'sometimes': cfg.?extra}"); // key absent when cfg.extra missing
7. String formatting & manipulation¶
var env = CelEnv.Create(new CelEnvSettings
{
Libraries = [Celly.Extensions.StringsLibrary.Instance],
});
'%s used %d of %d credits (%.1f%%)'.format([user, used, quota, 100.0 * used / quota])
// "amy used 42 of 100 credits (42.0%)"
'Hello, %s!'.format([name.substring(0, 1).upperAscii() + name.substring(1)])
path.split('/').filter(seg, seg != '') // path segments
parts.join('-') // reassemble
header.trim().lowerAscii() == 'application/json'
8. Custom functions: domain vocabulary¶
Give rule authors domain-specific verbs while keeping expressions simple:
using Celly.Stdlib;
var env = CelEnv.Create(new CelEnvSettings
{
Declarations = [new VariableDecl("card", CelType.Map(CelType.String, CelType.Dyn))],
ConfigureFunctions = registry =>
{
registry.Register("luhnValid", args => args is [StringValue s]
? BoolValue.Of(LuhnCheck(s.Value)) // your .NET code
: ErrorValue.NoSuchOverload());
registry.Register("riskScore", args => args is [StringValue country]
? IntValue.Of(RiskTable.GetValueOrDefault(country.Value, 50))
: ErrorValue.NoSuchOverload());
},
FunctionDeclarations =
[
new FunctionDecl("luhnValid", [new OverloadDecl("luhn_string", [CelType.String], CelType.Bool)]),
new FunctionDecl("riskScore", [new OverloadDecl("risk_string", [CelType.String], CelType.Int)]),
],
});
var screen = env.Compile("luhnValid(card.number) && riskScore(card.country) < 70");
9. A miniature rule engine¶
Many named rules, compiled once, evaluated together with shared bindings:
public sealed class RuleEngine
{
private readonly List<(string Name, CelProgram Program)> _rules = [];
private readonly CelEnv _env;
public RuleEngine(CelEnv env) => _env = env;
public IReadOnlyList<string> AddRule(string name, string expression)
{
var parsed = _env.Parse(expression);
if (parsed.Ast is null)
{
return [.. parsed.Issues.Select(i => i.ToString())]; // reject at ingest
}
var check = _env.Check(parsed.Ast);
if (check.HasErrors)
{
return [.. check.Issues.Select(i => i.ToString())];
}
_rules.Add((name, _env.Program(parsed.Ast)));
return [];
}
/// <summary>Names of all rules that fired. Errors count as "did not fire" (fail closed).</summary>
public IEnumerable<string> Evaluate(IReadOnlyDictionary<string, object?> facts) =>
_rules.Where(r => r.Program.Eval(facts) is BoolValue { Value: true }).Select(r => r.Name);
}
10. Caching compiled policies as protos¶
Skip re-parsing on every process start: check once, store the CheckedExpr bytes, and
rehydrate anywhere (including from ASTs produced by cel-go!).
using Celly.Protobuf;
using Google.Protobuf;
// Ingest: validate + serialize
var ast = env.Parse(ruleText).Ast!;
if (env.Check(ast).HasErrors) throw new ArgumentException("bad rule");
byte[] blob = AstConverter.ToCheckedExpr(ast).ToByteArray();
database.Save(ruleId, blob);
// Startup: rehydrate + plan (no parsing, no checking)
var restored = AstConverter.FromCheckedExpr(Cel.Expr.CheckedExpr.Parser.ParseFrom(blob));
var program = env.Program(restored);
11. Protobuf messages end to end¶
using Celly.Protobuf;
var registry = ProtoTypeRegistry.FromFiles(Order.Descriptor.File);
var env = CelEnv.Create(new CelEnvSettings
{
Container = "shop.v1", // Order's proto package
TypeProvider = registry,
Adapter = registry,
Declarations = [new VariableDecl("order", CelType.Struct("shop.v1.Order"))],
});
var program = env.Compile("""
order.status == OrderStatus.PAID &&
order.items.all(i, i.quantity > 0) &&
has(order.shipping_address) &&
order.total.units < 10000
""");
var result = program.Eval(new Dictionary<string, object?>
{
["order"] = myOrderMessage, // an IMessage — adapted automatically
});
12. Handling untrusted expressions safely¶
The full defensive pattern for user-supplied rules:
public static (CelProgram? Program, string? Error) TryCompile(CelEnv env, string text)
{
if (text.Length > 10_000)
{
return (null, "expression too long");
}
var parsed = env.Parse(text); // bounded recursion: hostile
if (parsed.Ast is null) // nesting is a parse error,
{ // never a crash
return (null, parsed.Issues[0].ToString());
}
var check = env.Check(parsed.Ast); // reject type errors and
if (check.HasErrors) // references to undeclared
{ // variables/functions
return (null, check.Issues[0].ToString());
}
// Optional: enforce a variable allow-list on top of declarations
var refs = Celly.Ast.AstTools.ReferencedVariables(parsed.Ast.Expr);
if (refs.Except(["request", "resource"]).Any())
{
return (null, "expression references unknown variables");
}
return (env.Program(parsed.Ast), null);
}
// At eval time: errors are values; decide your failure posture explicitly.
var outcome = program.Eval(facts) switch
{
BoolValue b => b.Value,
ErrorValue e => false, // fail closed (log e.Message)
_ => false, // non-bool result: treat as policy bug
};