PipelineModel¶
The [PipelineModel] attribute marks sealed records used in incremental generator pipelines. It ships with four analyzers that enforce correct equality semantics — preventing the most common causes of broken incremental caching.
See also: EquatableArray | Snapshots | Projections Overview
The Problem¶
Incremental source generators cache pipeline outputs and skip re-emit when inputs haven't changed. This only works if the pipeline model's Equals method returns true for semantically identical data. Three common patterns silently break this:
ImmutableArray<T>uses reference equality in recordsValidSymbol<T>wraps liveISymbol— broken equality + memory retentionISymbolretains the entireCompilationin memory- Non-
IEquatable<T>types fall back toobject.Equals(reference equality)
Usage¶
using Deepstaging.Roslyn;
[PipelineModel]
public sealed record MyModel
{
public required string Name { get; init; }
public required EquatableArray<MethodSnapshot> Methods { get; init; }
public required TypeSnapshot TargetType { get; init; }
}
The attribute itself has no runtime behavior — it's a marker for the analyzers.
Analyzer Rules¶
The Deepstaging.Roslyn NuGet package bundles these analyzers automatically. They run at compile time on any type decorated with [PipelineModel].
DSRK001 — ImmutableArray in Pipeline Model¶
| Severity | Error |
| Category | PipelineModel |
| Message | Property '{name}' on pipeline model '{type}' uses ImmutableArray<T> which has reference equality — use EquatableArray<T> instead |
ImmutableArray<T> is a struct with reference equality. In a record, this means two models with identical array contents compare as unequal, defeating caching.
Fix: Replace ImmutableArray<T> with EquatableArray<T>.
// ❌ DSRK001
public ImmutableArray<string> Names { get; init; }
// ✅ Fixed
public EquatableArray<string> Names { get; init; }
DSRK002 — ValidSymbol in Pipeline Model¶
| Severity | Error |
| Category | PipelineModel |
| Message | Property '{name}' on pipeline model '{type}' uses ValidSymbol<T> which retains the Compilation — use a snapshot type instead |
ValidSymbol<T> holds a reference to an ISymbol, which holds the entire Compilation — every syntax tree, symbol table, and metadata reference. Storing it in a pipeline model prevents garbage collection of old compilations.
Fix: Replace with the appropriate snapshot type and call .ToSnapshot() during projection.
// ❌ DSRK002
public required ValidSymbol<INamedTypeSymbol> TargetType { get; init; }
// ✅ Fixed
public required TypeSnapshot TargetType { get; init; }
DSRK003 — ISymbol in Pipeline Model¶
| Severity | Error |
| Category | PipelineModel |
| Message | Property '{name}' on pipeline model '{type}' uses an ISymbol type which retains the Compilation — extract data during the projection step |
Same memory retention problem as DSRK002, but for raw Roslyn symbol types (ISymbol, INamedTypeSymbol, IMethodSymbol, etc.).
Fix: Extract the data you need as strings or snapshot types during the projection step.
// ❌ DSRK003
public required INamedTypeSymbol Symbol { get; init; }
// ✅ Fixed — extract only what's needed
public required string SymbolName { get; init; }
public required string SymbolFullyQualifiedName { get; init; }
// ✅ Or use a snapshot if you need the full picture
public required TypeSnapshot Symbol { get; init; }
DSRK004 — Non-IEquatable Field¶
| Severity | Warning |
| Category | PipelineModel |
| Message | Property '{name}' on pipeline model '{type}' has type '{propertyType}' which does not implement IEquatable<T> — equality will be broken |
Types that don't implement IEquatable<T> fall back to object.Equals, which uses reference equality for classes. This silently breaks caching.
Fix: Ensure the property type implements IEquatable<T>. For custom types, use a sealed record (which auto-implements it) or implement the interface manually.
// ❌ DSRK004 — XElement doesn't implement IEquatable
public XElement? Documentation { get; init; }
// ✅ Fixed — use DocumentationSnapshot (a record with correct equality)
public DocumentationSnapshot Documentation { get; init; }
Note
DSRK004 excludes types already caught by DSRK001–DSRK003 to avoid duplicate diagnostics.
Guidelines¶
What Makes a Good Pipeline Model¶
sealed record— provides value equality via compiler-generatedEqualsEquatableArray<T>instead ofImmutableArray<T>— sequence equality- Snapshot types instead of
ValidSymbol<T>orISymbol— no Compilation retention - All field types implement
IEquatable<T>— no reference equality fallback - Computed properties use
=>— record equality ignores expression-bodied properties