S3L0001: Unpack the query

A long LINQ chain or a lambda nested in lambdas is unpacked into named steps.

Category Snail
Severity warning — an error under TreatWarningsAsErrors, which every Snail project builds with
Analyzer QueryShape in Snail.Sdk.Analyzers
Code fix none — naming the steps needs words only the author has

What it reports

  • A call chain of more than 4 LINQ operators whose receiver is a sequence: IEnumerable<T>, IQueryable<T>, IAsyncEnumerable<T> or IObservable<T>, or a type implementing one (arrays, lists and strings included). An operator is a call named like one of LINQ's — Select, Where, OrderBy, GroupBy, Skip, Take, Any, Count, First, ToList, ToDictionary, Distinct, Append, Contains and the rest of the list in QueryShape.cs. The finding is on the outermost call, once per chain: 5 operators in one chain: name the steps of this query with intermediate variables.
  • A lambda nested 2 deep: the third level of a => b => c => … is reported, a lambda nested 2 deep.

What it leaves alone

A chain of 4 operators or fewer, and any chain whose receiver is not a sequence: a StringBuilder's Append calls, a builder, a fluent API. Generated code is not analysed.

Why

A long chain reads as one breath: the reader has to run it in their head to know what each stage holds. A named variable per step says it, and gives the debugger a place to stop.

Measured across the fleet on 2026-10-12: 1.1 % of chains in src pass four operators and 0.6 % of lambdas nest two deep, so the threshold sits above the habit and reports the outliers.

How to fix

// Reported
var names = orders.Where(order => order.IsPaid).OrderBy(order => order.Date).Select(order => order.Customer).Distinct().Take(10).ToList();

// Holds
var paid = orders.Where(order => order.IsPaid).OrderBy(order => order.Date);
var customers = paid.Select(order => order.Customer).Distinct();
var names = customers.Take(10).ToList();

A nested lambda moves into a named local function or method.

Suppressing it

Fix the code rather than silence the rule: the rule is part of the standard every Snail repository keeps. Where one place truly must differ, suppress that place and say why:

[SuppressMessage("Snail", "S3L0001:Unpack the query", Justification = "<why this place differs>")]

A NoWarn in a project or props file would hide the rule from every new line too; the conformance check reports one as migration debt (S3LC0017).