Skip to content

The C# lineage ​

Greffon is a TypeScript rebuild of one C# idea: a lambda whose static type decides whether it compiles to a function or to data describing the function. C# has shipped that since 2007; LINQ rests on it, and EF Core is its largest deployed provider. This page maps what carried over, what EF Core specifically contributed, and where TypeScript forced a different answer.

Expression<Func<…>>, rebuilt at build time ​

In C#, the declared type picks what the compiler emits:

csharp
Func<User, bool>             f = u => u.Age > 18; // compiled code
Expression<Func<User, bool>> e = u => u.Age > 18; // an expression tree

TypeScript's compiler has no such seam, so Greffon adds one with a build transform: it finds lambdas at traced call sites and rewrites each into a literal carrying both forms — the original function, untouched, and the tree as plain data:

ts
db.users.filter((u) => u.age > minAge);
// ⇣ what the transform emits
db.users.filter(
  __tql_expr$({
    v: 1,
    compiled: (u) => u.age > minAge, // the function it always was
    params: ["u"],
    body: {
      kind: "Binary",
      op: ">",
      left: { kind: "Member", object: { kind: "Param", name: "u" }, prop: "age" },
      right: { kind: "Capture", name: "minAge" },
    },
    scope: () => ({ minAge }),
  }),
);

That transform runs in one of two build hosts: a bundler plugin (@greffon/vite, covering Vite, Rollup, and Rolldown) for a bundled build, or a TypeScript-compiler transformer (@greffon/ts-transformer) for a tsc-only backend with no bundler. Both emit the same literal.

Expr<(u: User) => boolean> is the counterpart of Expression<Func<User, bool>>, and every operator accepts F | Expr<F> — the same pairing C# expresses with the Func / Expression<Func> overload sets of Enumerable and Queryable.

IQueryable, plans, and explicit execution ​

A LINQ IQueryable chain runs nothing — it builds an expression tree that an IQueryProvider translates when the query is enumerated. Queryable keeps that split: every operator appends one op to an immutable QueryPlan, and a QueryProvider translates the plan when an executor runs.

One deliberate departure: C# executes on enumeration — foreach, ToList(), a stray Count() — and it is easy to run a query without meaning to. A Greffon Queryable is not a thenable and never auto-executes; I/O happens only at a named executor (toArray(), first(), count(), …). A line that queries the database should look like one.

Closures become parameters ​

C# lambdas capture variables by reference and read them when the query runs; EF Core then binds captured values as SQL parameters instead of pasting them into the statement. Both properties carry over. The emitted scope: () => ({ minAge }) thunk reads the live binding at execution time; partial evaluation folds it into the tree; SQL providers bind it as $1 / ?:

ts
let minAge = 18;
const adults = db.users.filter((u) => u.age > minAge);
minAge = 21;
await adults.toArray(); // WHERE "users"."age" > $1 — with $1 = 21, the value at execution

What EF Core contributed ​

EF Core is the reference implementation for "LINQ over a real database", and Greffon borrows its answers directly:

  • include / thenInclude are EF Core's navigation-loading names and rules — includes attach to result rows and are invisible to filter in the same query. Greffon always executes them the way EF Core's AsSplitQuery() does: one batched statement per navigation, so joins never duplicate parents and take/skip apply to parents alone.
  • flatMap is SelectMany — querying through a navigation becomes a join.
  • .inMemory() is AsEnumerable(), made mandatory. Pre-Core EF silently finished untranslatable queries on the client; the accidental table scans were bad enough that EF Core 3.0 removed the behavior as a breaking change. Greffon starts where that story ended: untranslatable residue is a located, coded error, and rows cross into JavaScript only at the explicit .inMemory() boundary.
  • The in-memory implementation is the semantics. LINQ to Objects defines what the operators mean, and remote providers are judged against it. @greffon/provider-memory plays the same role — enforced by a property-based conformance suite rather than by convention.

Where TypeScript forced different answers ​

  • A build step instead of a compiler feature. The C# compiler builds trees; Greffon's are built by a bundler plugin or the tsc transformer — or, without either, by a runtime toString() fallback that is closure-blind and says so (R3002). What C# gets from the compiler being the single validator, Greffon rebuilds by sharing one validator package across the build, the editor plugin, and the ESLint rule.
  • A closed, serializable grammar instead of an open one. A C# expression tree can reference any .NET method, and it does not serialize — it lives and dies in-process. Greffon's tree is a small closed algebra with a versioned JSON wire format, because these trees are meant to leave the process: cross to a server, sit in a policy store, be translated by third-party providers. A provider can promise translation only over a finite grammar.
  • Different subset lines. C#'s converter has restrictions of its own — no statement bodies, no assignments, no ?.. Greffon's subset is the same idea with the lines drawn for cross-provider meaning, and optional chaining is inside it, because navigation properties are optional.

The map ​

The types and architecture carry across one-to-one:

C# / .NETGreffon
Expression<Func<User, bool>>Expr<(u: User) => boolean>
Compiler-built expression treesBuild-time reification (@greffon/vite, @greffon/ts-transformer)
IQueryable<T>Queryable<T>
IQueryProviderQueryProvider
LINQ to Objects@greffon/provider-memory
ExpressionVisitorThe visitor / rewriter in @greffon/core
Include / ThenInclude, AsSplitQueryinclude / thenInclude (always split)

Operators, three ways ​

LINQ named its operators after SQL (Where, Select). Greffon names them after the JavaScript Array methods they mirror, because the same lambda already runs against a plain array in the memory provider — so a query reads the way the equivalent array transform reads. The LINQ name is kept in the last column for anyone arriving from C#:

GreffonJavaScript ArrayLINQ (C#)
filter(p)Array.prototype.filterWhere
map(s)Array.prototype.mapSelect
flatMap(nav)Array.prototype.flatMapSelectMany
orderBy(k) / orderByDescending(k)Array.prototype.sort (by a key)OrderBy / OrderByDescending
thenBy(k) / thenByDescending(k)a stable sort on the previous keyThenBy / ThenByDescending
take(n) / skip(n)slice(0, n) / slice(n)Take / Skip
distinct()[...new Set(xs)]Distinct
groupBy(k)Object.groupBy / Map.groupByGroupBy
join(...) / leftJoin(...)hand-written (Array.join is string-joining)Join / GroupJoin
include / thenInclude— (loads a declared navigation)Include / ThenInclude

The executors — the terminal calls that actually run the query — follow the same rule:

GreffonJavaScript ArrayLINQ (C#)
some(p?)Array.prototype.someAny
every(p)Array.prototype.everyAll
first(p?) → T | nullArray.prototype.findFirstOrDefault
firstOrThrow(p?)—First
single(p?)—Single
count(p?)Array.prototype.lengthCount
sum(s)Array.prototype.reduceSum
min(s) / max(s)Math.min / Math.max over the keysMin / Max
avg(s)reduce ÷ lengthAverage
toArray()[...xs]ToList / ToArray
.inMemory()— (crosses into the array world)AsEnumerable

Three names deliberately keep a non-Array spelling, because no array method carries the same meaning: orderBy takes a key selector and sorts stably across levels rather than mutating in place like sort; groupBy returns Grouping values (and predates Object.groupBy); and join is the relational join, since Array.prototype.join already means string concatenation. The executor names were settled first — some/every over LINQ's Any/All, and a nullable first over a throwing one — and filter/map extend that same JS-first convention to the operators.

What Greffon does not rebuild is the rest of EF Core: no DbContext change tracking, no SaveChanges, no migrations, no write path at all. That split is deliberate — Compared to ORMs & rules engines covers it.

MIT licensed. Expression trees for TypeScript.