logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·01.11.26
← Back to news

Building a native C# implementation of CEL engine

Original article
bsid.io|38 points|8 comments|by jackedEngineer|Jul 27, 2026

Engineering a Native C# Implementation of the CEL Engine

By Siddhartha Bariker
Published: July 18, 2026

Executive Summary

I have developed Celly, a ground-up, native C# implementation of Google’s Common Expression Language (CEL). Not only does it achieve 100% compliance with the official conformance suite, but its development also led to the creation of the first .NET version of the protovalidate library.


Understanding CEL: The "What" and "Why"

CEL is a concise, secure expression language designed by Google. While you might not have heard of it, you likely interact with systems powered by it daily. It serves as the logic layer for:

  • Kubernetes admission policies
  • Envoy RBAC configurations
  • Google Cloud IAM conditions
  • gRPC protovalidate

The core philosophy is to treat logic as data. Instead of compiling code, you store expressions as strings in configuration files and evaluate them safely at runtime.

Example Expression:
request.auth.groups.exists(g, g == 'admin') || resource.owner == request.auth.subject

The Dilemma of External Logic

When you need business logic to exist outside your compiled binary (e.g., "Who can access this?" or "Is this payload valid?"), you usually face three bad options:

ApproachThe "Trap"Result
HardcodingEvery rule change requires a full CI/CD cycle.Business agility is throttled by deployment speed.
ScriptingGiving users a full language (like Python/JS).Risk=Arbitrary Code Execution\text{Risk} = \text{Arbitrary Code Execution}Risk=Arbitrary Code Execution (Infinite loops, memory exhaustion).
Custom DSLBuilding your own parser and type system.You are now the maintainer of a proprietary language spec.

Why CEL is the "Sweet Spot"

CEL avoids these pitfalls through deliberate restraint:

  1. Guaranteed Termination: It is non-Turing complete, meaning it always finishes. This is why infrastructure giants trust it with user-supplied input.
  2. Purely Functional: Expressions only operate on provided data and return a value.
  3. Portable: A rule written in CEL behaves identically whether the engine is written in Go, C++, Java, Rust, or now, C#.
  4. Strongly Typed: Errors are caught during the type-checking phase, before the rule is ever executed.

The Challenge of .NET Implementation

While official implementations exist for several languages, .NET was left behind. Existing options were merely partial ports that failed the official conformance tests.

The "No Shortcuts" Mandate

I faced a tempting shortcut: compile the Go implementation to WebAssembly (WASM) and wrap it in C#. I rejected this because:

  • It introduces a heavy WASM runtime dependency.
  • It creates complex packaging and platform-specific hurdles.

Instead, I committed to a pure managed implementation. This meant building every component from scratch: Lexer →\rightarrow→ Recursive-Descent Parser →\rightarrow→ Type Checker →\rightarrow→ Tree-Walking Evaluator.


Ensuring Absolute Correctness

Claiming "100% support" is a heavy burden. CEL contains numerous subtle edge cases that can break a naive implementation:

  • Numeric Precision: Handling cross-type comparisons (e.g., 1==1u==1.01 == 1u == 1.01==1u==1.0) without losing precision above 2532^{53}253 by avoiding intermediate double casts.
  • Unicode Handling: .NET uses UTF-16, but CEL functions like size() and substring() must operate on Unicode code points to avoid breaking surrogate pairs.
  • Temporal Precision: CEL requires nanosecond precision, whereas .NET Ticks operate at 100ns100\text{ns}100ns intervals.
  • String Formatting: Ensuring string(1e23) matches Go's strconv scientific notation exactly.
  • Boolean Logic: Implementing and and or as commutative and error-absorbing (e.g., false || error must result in false).

The Verification Pipeline

To prove Celly's correctness, I implemented a rigorous testing strategy:

  • The Conformance Suite: Integrated the official cel-spec (2,456 tests).
  • Ratcheting Skip-List: Failing tests were tracked; once a test passed, it was locked in, ensuring progress never regressed.
  • Independent Audit: A separate tool pulled Celly from NuGet to verify test data.
  • Differential Fuzzing: Generated tens of thousands of random expressions, comparing Celly's output against cel-go byte-for-byte.

Celly in Action

Here is how you utilize Celly in a .NET project:

using Celly;
using Celly.Checking;
using Celly.Types;

// 1. Setup the environment and declarations
var env = CelEnv.Create(new CelEnvSettings { 
    Declarations = [ 
        new VariableDecl("request", CelType.Map(CelType.String, CelType.Dyn))
    ]
});

// 2. Compile the expression (once)
var program = env.Compile("request.user.startsWith('admin-') && size(request.groups) > 0");

// 3. Evaluate against dynamic data (many times)
var result = program.Eval(new Dictionary<string, object?> { 
    ["user"] = "admin-alice", 
    ["groups"] = new[] { "ops" }, 
}); 

// result == true

Performance Architecture

The primary goal of a policy engine is to minimize the overhead of the request path. Celly achieves this by separating the expensive work from the execution.

The CelProgram is immutable and thread-safe, allowing a single compiled instance to serve thousands of concurrent requests with minimal latency.