Skip to content

Compile & evaluate

This guide is the practical companion to the evaluation model page. Each section is a recipe.

Recipe: simplest possible program

using DotnetCel;
using DotnetCel.Types;
var env = CelEnv.NewBuilder()
.Variable("x", CelTypes.Int)
.Build();
var program = CelExpression.Compile("x * 2", env);
var result = program.Eval(new Dictionary<string, object?> { ["x"] = 21 });
Console.WriteLine(result); // 42

Recipe: anonymous-object activation

var program = CelExpression.Compile(
"user.name + ' (' + string(user.age) + ')'",
CelEnv.NewBuilder()
.Variable("user", CelTypes.Object("User"))
.Build());
var greeting = (string)program.Eval(new
{
user = new { Name = "alice", Age = 30 }
})!;

The anonymous object’s top-level properties (user) become CEL variables; nested field access (user.name) is resolved by the POCO adapter — see Working with POCOs.

Recipe: reuse a program across requests

public sealed class FraudCheck
{
private readonly CompiledProgram _program;
public FraudCheck(string rule)
{
var env = CelEnv.NewBuilder()
.Variable("event", CelTypes.Object("Event"))
.Variable("user", CelTypes.Object("User"))
.Build();
_program = CelExpression.Compile(rule, env);
}
public bool IsSuspicious(Event evt, User user) =>
(bool)_program.Eval(new { @event = evt, user })!;
}

CompiledProgram is thread-safe for evaluation — one shared instance can serve many concurrent requests.

Recipe: lots of programs, one env

var env = CelEnv.NewBuilder()
.Variable("event", CelTypes.Object("Event"))
.Build();
var programs = rules.ToDictionary(
r => r.Id,
r => CelExpression.Compile(r.Source, env));
bool Allowed(string ruleId, Event evt) =>
programs.TryGetValue(ruleId, out var p)
&& (bool)p.Eval(new { @event = evt })!;

Recipe: extending an env

CelEnv is immutable. Use Extend() to derive a child without rebuilding from scratch:

var baseEnv = CelEnv.NewBuilder()
.Use(StringsExtension.Instance)
.Build();
var requestEnv = baseEnv.Extend()
.Variable("request", CelTypes.Object("Request"))
.Build();
var responseEnv = baseEnv.Extend()
.Variable("response", CelTypes.Object("Response"))
.Build();

Both child envs share the standard library and the strings extension; each adds its own variable.

Recipe: handling compile-time errors

try
{
var program = CelExpression.Compile(userSubmittedRule, env);
}
catch (CelCompileException ex)
{
foreach (var diag in ex.Diagnostics)
{
Console.WriteLine($"{diag.Line}:{diag.Column} {diag.Severity} {diag.Message}");
}
}

Diagnostics include line/column positions sourced from the parser’s source info. They’re suitable to surface in a UI as “your rule has these problems”.

Recipe: handling runtime errors

Most operator-level mishaps are absorbed by short-circuiting. The cases that do escape get wrapped in CelEvaluationException:

try
{
var v = program.Eval(activation);
}
catch (CelEvaluationException ex)
{
log.Warning("CEL rule failed: {Message}", ex.Message);
return _defaults.Deny;
}

See Errors & unknowns for what does and doesn’t short-circuit.

Recipe: returning rich values

The default Eval(...) overloads unwrap to a CLR object. If you want the full CelValue (e.g. to distinguish null from “key not present”), use EvaluateRaw:

using DotnetCel.Runtime;
using DotnetCel.Values;
CelValue raw = program.EvaluateRaw(new MapActivation(bindings));
switch (raw)
{
case BoolValue b: return b.Value;
case IntValue i: return i.Value;
case ListValue l: return l.Elements;
case OptionalValue o when o.HasValue: return o.Inner;
case OptionalValue: return null;
case ErrorValue e: throw new InvalidOperationException(e.Message);
case UnknownValue: return _placeholder;
default: return raw.ToClrObject();
}

This is the right shape for layered rule engines that pass intermediate CEL results between stages without round-tripping through CLR types.

Recipe: lifecycle in a long-running service

public sealed class RuleEngine : IAsyncDisposable
{
private readonly Dictionary<string, CompiledProgram> _programs = new();
private readonly CelEnv _env;
public RuleEngine(CelEnv env) => _env = env;
public void Load(string id, string source)
{
_programs[id] = CelExpression.Compile(source, _env);
}
public bool Check(string id, IActivation activation) =>
(bool)_programs[id].Eval(activation)!;
public ValueTask DisposeAsync()
{
_programs.Clear(); // CompiledProgram has no unmanaged resources.
return ValueTask.CompletedTask;
}
}

There is no native cleanup needed — CompiledProgram is pure managed state.

What can go wrong

SymptomLikely causeFix
undeclared reference to 'x' at compileVariable not on env, or wrong container.Variable("x", ...) or set container
no matching overload at compileOperator/function called with wrong typescheck inputs; consider dyn(...) if intended
Value comes back as dynField is on an opaque object typeregister ITypeProvider, or live with dyn
CelEvaluationException at evalTop-level error not absorbed by ||/&&/?:wrap with has(...) or rewrite the rule
Slow Compile in a loopYou’re compiling per-requestcache the CompiledProgram