Declaring variables
A CEL variable is the bridge between your host code and an expression. Every identifier in the CEL source must resolve to something on the env — typically a variable declaration. This guide is the cookbook.
The two halves
A variable has two halves:
- Declaration — name + CEL type. Lives on
CelEnv. Used by the checker. - Binding — name + CLR value. Lives in the
IActivationyou pass toEval. Used by the runtime.
Mismatch between the two is your responsibility. The checker doesn’t see the
runtime values, and the runtime doesn’t enforce that bound values match
declared types. (It mostly does the right thing because CelValue’s
discriminator tells the runtime what type each value is — but
Variable("x", CelTypes.Int) then bindings["x"] = "hello" will produce
runtime errors when an int is expected.)
The simplest case
var env = CelEnv.NewBuilder() .Variable("count", CelTypes.Int) .Variable("label", CelTypes.String) .Build();
var program = CelExpression.Compile("label + ': ' + string(count)", env);
var s = (string)program.Eval(new Dictionary<string, object?>{ ["count"] = 42, ["label"] = "answer",})!;// "answer: 42"Compound types
.Variable("tags", CelTypes.List(CelTypes.String)).Variable("counters", CelTypes.Map(CelTypes.String, CelTypes.Int)).Variable("user", CelTypes.Object("User")).Variable("flagged", CelTypes.Optional(CelTypes.Bool))The CLR-side bindings:
new Dictionary<string, object?>{ ["tags"] = new[] { "a", "b" }, // any IEnumerable<string> ["counters"] = new Dictionary<string, int> { ["a"] = 1 }, // any IDictionary ["user"] = userPoco, // any object ["flagged"] = true, // bool? null = "no value"}null is allowed for wrapper, optional, and object types. It
will produce runtime errors if used where a non-nullable primitive is
expected.
Object types
There are three flavours of “object type” depending on how strict you want to be:
// 1. Opaque — checker treats fields as dyn, runtime uses POCO reflection..Variable("user", CelTypes.Object("User"))
// 2. Provider-backed — checker validates field names and types..UseTypeProvider(new MyProvider()).Variable("user", CelTypes.Object("User"))
// 3. Anonymous root — top-level properties of an object become variables.program.Eval(new { user = ..., request = ... })See Working with POCOs for the trade-offs.
Container-qualified names
Set a Container on the env to give the checker a default namespace:
var env = CelEnv.NewBuilder() .SetContainer("acme.v1") .Variable("acme.v1.request", CelTypes.Object("acme.v1.Request")) .Build();
var program = CelExpression.Compile("request.user.name", env);The checker resolves request by walking candidate names: first the
unqualified request, then acme.v1.request. The longest match that
matches a declared variable wins. This mirrors proto’s package resolution
rules.
Variable shadowing
Comprehension iter variables and cel.bind accumulator names take
precedence over variables of the same name from the env. So:
items.map(items, items.id)// ^ ^// iter refers to iter, not the outer 'items' listThis is a CEL spec rule, not a .NET-specific quirk.
Renaming or deriving variable types
CelEnv is immutable, but Extend() returns a builder seeded from the
existing env. Use it to derive specialized envs for different rule
categories:
var baseEnv = CelEnv.NewBuilder() .Use(StringsExtension.Instance) .Build();
var requestEnv = baseEnv.Extend() .Variable("request", CelTypes.Object("acme.v1.Request")) .Build();
var responseEnv = baseEnv.Extend() .Variable("response", CelTypes.Object("acme.v1.Response")) .Build();Listing what’s declared
foreach (var (name, decl) in env.Variables){ Console.WriteLine($"{name}: {decl.Type}");}This is the env’s “schema” surface — useful for building rule-authoring UIs that autocomplete variable names.
Common mistakes
- Forgetting to declare — a CEL identifier that has no env entry produces
undeclared reference to 'x'at compile time. - Wrong CEL type —
Variable("count", CelTypes.String)thenbindings["count"] = 42produces runtime errors whencountis used as a string. Pick the type that matches your data. - Treating object types as schema —
CelTypes.Object("User")does not describeUser’s fields. To describe them, register a type provider. - Mixing case — CEL is case-sensitive.
Countandcountare different.
See also
CelEnvreference — the full builder API.- Activations — how variables are looked up at runtime.
- Working with POCOs — when to use
Object("...")vs. anonymous-root activation.