ADR 0048: Fluent Cypher aggregates and bounded collect¶
Date: 2026-10-16. Status: Accepted with the verification below. Extends the WITH aggregation decision in ADR 0041, which introduced counts only.
Context¶
Fluent Cypher could introduce count(...) in WITH, but not the other common aggregates. Questions such as "the earliest and latest release per actor" or "the titles of an actor's movies" had to fall back to native Cypher. Two hazards shape the design. First, aggregates have native null rules that differ from LINQ: min, max and avg of an all-null group are null, sum is 0 and collect skips nulls. Second, collect returns a list whose size is unbounded, although Fluent bounds every result with LIMIT.
Decision¶
Add Graph.Min, Graph.Max, Graph.Sum, Graph.Avg, Graph.Collect and Graph.CollectDistinct. Each returns a CypherValue<T> introduced by With(...), like Graph.Count:
| API | Cypher | Value type | Null rule |
|---|---|---|---|
Min(expr), Max(expr) |
min(x), max(x) |
The property's type | Null for an all-null group, for example after OPTIONAL MATCH; project (int?)value.Value |
Sum(expr) |
sum(x) |
long for int/long properties, double for double |
Never null: an all-null group sums to 0 |
Avg(expr) |
avg(x) |
double? |
Null for an all-null group |
Collect(expr, max), CollectDistinct(expr, max) |
collect(x)[..$max] |
IReadOnlyList<T> |
Nulls skipped; empty for an all-null group |
- Semantics are native. These are explicit native aggregates, not C# lambdas. In later lambda predicates, ADR 0047 treats
sumas never null andmin,maxandavgas possibly null, so!=on them is rejected (GBFC110). collectmust be bounded. The maximum (1–10,000) is required and is applied on the server by list slicing, so a single row cannot carry an unbounded list. Element order is not guaranteed.- Readers use the aggregated property's binding.
min,maxandcollectresults are read like the property itself, so Neo4j's 64-bit integers return asintwhere the property isint, andcollectover a converted property (such as aGuidkey stored as a string) returns model values. This makesCollect(key, n)a bounded way to return keys for the keys-to-EF bridge. - Aggregates can be returned without WITH. A
CypherValueaggregate used only inReturnrenders inline, and native Cypher groups by the returned non-aggregate columns. The result equals the WITH form; WITH remains necessary to filter on an aggregate. Counts follow the same rule. - Converted properties are refused for
min,max,sumandavg(GBFC406): the server would aggregate the stored form, which a converter does not order or add like the model value. The operand must be a mapped property expression (GBFC406).
Not adopted¶
Aggregates over arbitrary expressions, percentileCont/stDev, and WITH grouping keys other than forwarded symbols (a RETURN groups by whatever non-aggregate columns it returns). They remain native Cypher until application evidence asks for them.
Verification¶
- Unit: rendering of every aggregate in WITH with a lambda
Whereover the values; GBFC110 for!=onmin; GBFC406 for a converted operand; collect bounds; collect over a converted property. - Live (
ProviderFluentLambdaTests.Aggregates_read_typed_values_and_native_null_groups): per-personmin,max,sum,avgandcollectafter OPTIONAL MATCH equal a C# oracle over the seed rows, including people without credits (null, 0 and an empty list), withGuidkeys read back through their converter, and the same aggregates returned without WITH equal the WITH form row for row.