Skip to content

CelValue

DotnetCel.Values.CelValue is an abstract record whose subtypes form a closed sum. Every value the evaluator produces or consumes is exactly one of these types.

The hierarchy

public abstract record CelValue
{
public abstract CelType Type { get; }
public abstract object? ToClrObject();
}
SubtypeCEL typeCLR projection
NullValuenull_typenull
BoolValueboolbool
IntValueintlong
UintValueuintulong
DoubleValuedoubledouble
StringValuestringstring
BytesValuebytesbyte[]
DurationValuedurationCelDuration
TimestampValuetimestampCelTimestamp
ListValuelist<T>List<object?>
MapValuemap<K, V>Dictionary<object, object?>
ObjectValuenamed object typethe wrapped CLR instance
EnumValueobject type (enum)long (the enum’s number)
OptionalValueoptional<T>inner value or null
TypeValuetypethe wrapped CelType
ErrorValueerrorthrows CelEvaluationException
UnknownValuedynnull

Constructing values

The CelValue static class exposes factory helpers:

CelValue.Null;
CelValue.True;
CelValue.False;
CelValue.Of(true);
CelValue.Of(42L); // → IntValue
CelValue.Of(42UL); // → UintValue
CelValue.Of(3.14); // → DoubleValue
CelValue.Of("hello"); // → StringValue
CelValue.Of(new byte[] {1, 2, 3}); // → BytesValue
CelValue.Of(new CelDuration(...));
CelValue.Of(new CelTimestamp(...));
CelValue.Error("message", code: "OPT");
new ListValue([...elements...]);
new MapValue([...entries...].ToImmutableDictionary());
new ObjectValue("acme.v1.User", userInstance);
new EnumValue("acme.v1.GlobalEnum", 2);
new OptionalValue(inner: CelValue.Of(42));
OptionalValue.None;
new TypeValue(CelTypes.Int);

Type property

Each subtype’s Type returns its CEL static type:

CelValue.Of(42L).Type // CelTypes.Int
CelValue.Of("hi").Type // CelTypes.String
new ListValue(...).Type // CelTypes.List(CelTypes.Dyn)
new ObjectValue("User", u).Type // CelTypes.Object("User")
new EnumValue("E", 2).Type // CelTypes.Object("E")

This is what type(x) reflects on at runtime.

ToClrObject projection

Returns a sensible CLR representation:

CelValue.Of(42L).ToClrObject() // 42L (long)
CelValue.Of("hi").ToClrObject() // "hi"
new ListValue([...]).ToClrObject() // List<object?>
new MapValue(...).ToClrObject() // Dictionary<object, object?>
new ObjectValue("User", u).ToClrObject() // u (the original instance)

ErrorValue.ToClrObject() throws CelEvaluationException — this is what makes program.Eval(...) raise on top-level errors. To inspect an error without throwing, pattern-match on the ErrorValue instead.

Equality

CelValue records use C#‘s built-in record equality by default, but CEL semantics differ in important ways (cross-numeric 1 == 1.0, NaN asymmetry, list/map structural compare). Use DotnetCel.Runtime.CelEquality for any “is the CEL spec semantics” comparison:

DotnetCel.Runtime.CelEquality.Equals(CelValue.Of(1L), CelValue.Of(1.0));
// → true (CEL says yes)
CelValue.Of(1L) == CelValue.Of(1.0);
// → false (record equality is type-then-value)

Special-case rules

  • NaN ≠ anything, including itself. Per IEEE 754; CEL adopts this.
  • null == null → true (one of the few comparisons that survives).
  • Enums compare to ints by numberEnumValue(_, 2) == IntValue(2).
  • Lists/maps compare structurally, recursively using these same rules.
  • Object equality routes through the ITypeProvider’s AreEqual hook when one is registered.

Pattern-matching results

The closed-sum nature makes pattern-matching ergonomic:

return result switch
{
BoolValue b => b.Value ? "yes" : "no",
IntValue { Value: 0 } => "zero",
IntValue i => $"int {i.Value}",
StringValue s => s.Value,
ListValue l => $"{l.Elements.Length} items",
OptionalValue { HasValue: false } => "missing",
OptionalValue o => Format(o.Inner!),
ErrorValue e => throw new InvalidOperationException(e.Message),
_ => result.ToClrObject()?.ToString() ?? "null",
};

See also