Table of Contents

Value transforms

This page documents Orleans.Lattice.Schema 9.9.0, in the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at value-transforms.md, and llms.txt lists every page.

LatticeValueTransform is a small, serializable intermediate representation (IR) that describes how to rewrite one JSON value into another. It is the single primitive shared by both schema capabilities:

  • enforcement remediation applies a transform to every existing value when bringing a tree into compliance with a new policy, and
  • versioning upcasters apply a transform to lift a stale value from one schema version to the next.

Because the transform is a serializable IR (a sibling of the LatticePredicateNode predicate IR), it can be persisted on the durable remediation coordinator and replayed identically after a failover, and it can be evaluated server-side against a value's JSON document with no client callback.

Building a transform

Compose a transform from these factories. The root of every transform is a Passthrough; the member operations inside it address top-level members of the document, and value expressions always read from the input document, never the partially rewritten output:

Factory Effect
Passthrough(ops...) Copy the input document, then apply zero or more member operations in order, leaving everything else intact.
SetMember(path, valueExpr) Set (or add) the top-level member path to the value produced by valueExpr.
DropMember(path) Remove the top-level member path.
RenameMember(from, to) Move the top-level member from to to.
Member(path) A value expression reading the top-level member path of the input document.
Const(constant) A value expression yielding a constant.
Compute(op, operands...) A value expression computing over its operands (Concat, Coalesce).
Conditional(predicate, then, else) A value expression yielding then when the LatticePredicateNode matches the input document, otherwise else.
using Orleans.Lattice.Schema;

// Add a default "status": "open", rename "qty" to "quantity", drop "legacy".
var transform = LatticeValueTransform.Passthrough(
    LatticeValueTransform.SetMember(
        "status", LatticeValueTransform.Const(LatticeConstant.Text("open"))),
    LatticeValueTransform.RenameMember("qty", "quantity"),
    LatticeValueTransform.DropMember("legacy"));

LatticeValueTransformEvaluation.Evaluate(value, transform) applies a transform to one value in process - the same evaluation remediation and upcasting run - and throws InvalidOperationException for a null, empty or malformed JSON payload, a structurally invalid transform, or one nested more than 128 levels deep. LatticeSchemaRemediation.DryRunAsync runs that evaluation and a candidate policy's check over a stream of entries, stopping at the first value that fails, the way a remediation's dry-run gate does, without a cluster; it returns a LatticeSchemaRemediationOutcome.

Lowering from a lambda

For the common case you do not hand-build the IR: write an ordinary Expression<Func<TOld, TNew>> and let LatticeValueTransformTranslator lower it to the IR. The translator is allowlisted - an expression it cannot represent throws NotSupportedException at translation time rather than failing later on the server.

using System.Linq.Expressions;
using Orleans.Lattice.Schema;

Expression<Func<Order, Order>> upgrade = o => new Order(o.Id, o.Total);
LatticeValueTransform transform = LatticeValueTransformTranslator.Translate<Order>(upgrade);

The DI escape hatch

Some conversions cannot be expressed in the IR - arbitrary computation, or opaque / non-JSON payloads. For those, implement ILatticeValueTransform (a byte[] -> byte[] transform with a stable Id), register it with AddLatticeValueTransform(...) (on the silo builder or the service collection), and reference it by id from a versioning upcaster (AddUpcaster(schemaId, fromVersion, toVersion, transformId)). The registry resolves the id to your implementation at evaluation time, so the same escape hatch works on the durable eager version-migration path. An enforcement remediation takes only the IR: RemediateAsync accepts a LatticeValueTransform, so a DI-registered transform cannot drive one.

See also