Skip to content

Repository files navigation

Cel for .NET

An idiomatic C# / .NET 10 implementation of the Common Expression Language.

CEL is Google's "safe expression" language for policy, validation, and rule engines: small, sandboxed, totally evaluated, and stable across implementations. This port targets full conformance with the spec and treats POCOs as first-class — protobuf is optional.

Quick start

1. Hello, world

Declare what variables an expression can reference, compile, and evaluate:

usingDotnetCel;usingDotnetCel.Types;varenv=CelEnv.NewBuilder().Variable("name",CelTypes.String).Build();varprogram=CelExpression.Compile("'hello, ' + name",env);vargreeting=(string)program.Eval(newDictionary<string,object?>{["name"]="world",})!;// "hello, world"

2. A predicate over POCOs

Plain CLR objects bind directly — no schema, no codegen, no protobuf. Top-level properties of an anonymous root become CEL variables; nested field access is resolved at runtime by the reflection-backed POCO adapter:

usingDotnetCel;usingDotnetCel.Extensions;usingDotnetCel.Types;publicsealedrecordUser(stringName,intAge,string[]Roles);varenv=CelEnv.NewBuilder().Use(StringsExtension.Instance).Variable("user",CelTypes.Object("User")).Build();varprogram=CelExpression.Compile("user.Name.startsWith('a') && user.Age >= 18 && 'admin' in user.Roles",env);boolallowed=(bool)program.Eval(new{user=newUser("alice",25,["admin","user"]),})!;

CelExpression.Compile is the slow step (~50–200 µs); the returned CompiledProgram is thread-safe and meant to be reused across millions of evaluations.

3. Field naming — JsonPropertyName + conventions

Map CLR PascalCase properties to CEL expressions in any case style. Per-member overrides via [JsonPropertyName]; per-env defaults via UsePocoNaming(...):

usingSystem.Text.Json.Serialization;publicsealedclassAccount{[JsonPropertyName("user_name")]publicstringUserName{get;init;}="";publicintAge{get;init;}[JsonIgnore]publicstringSessionToken{get;init;}="";}varenv=CelEnv.NewBuilder().UsePocoNaming(PocoNamingConvention.SnakeCase).Variable("acc",CelTypes.Object("Account")).Build();// CEL field names: user_name (from attribute), age (from convention).// session_token is hidden by [JsonIgnore].varprogram=CelExpression.Compile("acc.user_name == 'alice' && acc.age >= 18",env);

4. Comprehensions

map, filter, all, exists, exists_one work over lists and map keys:

varprogram=CelExpression.Compile("items.filter(i, i.in_stock && i.price < 100).size() > 0",env);boolhasAffordable=(bool)program.Eval(new{items=new[]{new{in_stock=true,price=30},new{in_stock=false,price=50},new{in_stock=true,price=200},}})!;

5. Custom functions

Extend the language with your own functions via ICelExtension:

usingDotnetCel;usingDotnetCel.Types;usingDotnetCel.Values;publicsealedclassGreetExtension:ICelExtension{publicstaticreadonlyGreetExtensionInstance=new();privateGreetExtension(){}publicvoidConfigureEnv(CelEnv.Builderb)=>b.Function("greet",newOverloadDecl("greet_string",[CelTypes.String],CelTypes.String));publicvoidConfigureRuntime(Action<string,OverloadFn>bind)=>bind("greet_string",
args =>CelValue.Of($"hello, {((StringValue)args[0]).Value}"));}varenv=CelEnv.NewBuilder().Use(GreetExtension.Instance).Build();CelExpression.Compile("greet('alice')",env).Eval(newDictionary<string,object?>());// "hello, alice"

Documentation

Full docs live at https://suhdev.github.io/csharp-cel/ (Astro Starlight, sources under docs/) — getting-started, concepts, guides, and a complete API reference. Run locally with cd docs && npm install && npm run dev.

Layout

  • src/DotnetCel.Core — AST, type system, value model, diagnostics. No external deps.
  • src/DotnetCel.Parser — lexer + Pratt parser, macro expansion.
  • src/DotnetCel.Checker — type checker, declarations, overload resolution.
  • src/DotnetCel.Runtime — tree-walking evaluator, activations, POCO adapter, stdlib.
  • src/DotnetCel.Extensions — strings, math, encoders, sets, optionals, bindings, network, block.
  • src/DotnetCel — public façade (CelExpression, CompiledProgram).
  • tests/DotnetCel.UnitTests — unit tests (181 cases).
  • tests/DotnetCel.Conformance — runs the cel-spec textproto conformance corpus.
  • docs/ — Astro Starlight documentation site.

Conformance

Run the harness against a checkout of cel-spec (sibling repo by default):

dotnet run --project tests/DotnetCel.Conformance
# or with a custom path:
dotnet run --project tests/DotnetCel.Conformance -- /path/to/cel-spec/tests/simple/testdata
# or specific files:
dotnet run --project tests/DotnetCel.Conformance -- ../cel-spec/tests/simple/testdata --only basic comparisons

Current pass rate (cel-spec corpus, all 30 files)

CategoryStatus
basic, bindings_ext, comparisons, enums, fp_math, integer_math, lists, logic, macros, network_ext, parse, plumbing, string100%
block_ext, conversions, dynamic, fields, macros2, math_ext, proto3, string_ext, timestamps, wrappers85–98%
encoders_ext, namespace, optionals, proto273–89%
proto2_ext, type_deduction, unknowns0% — feature gaps
Total2082 / 2257 ran (92%) over 2454 cases (197 skipped)

Known gaps

  • unknowns — partial-evaluation infrastructure. The runtime represents unknowns internally (UnknownValue); public API for emitting them from activations is pending.
  • type_deductiontyped_result matcher in conformance harness not yet wired up; CompiledProgram.ResultType exists but the spec corpus doesn't drive it.
  • proto2_ext — proto2 message extensions (the extend block) — parser + provider lookup not yet implemented.
  • Reflection POCO adapter — annotated [RequiresUnreferencedCode]; SourceGen variant pending for AOT/trim.

About

A C# implementation of CEL

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages