Skip to content

Snapshots

Pipeline-safe materializations of Roslyn symbols. Snapshots capture all needed symbol data as primitives, strings, and records — safe for use in incremental generator pipeline models.

See also: ValidSymbol | EquatableArray | PipelineModel

The Problem

ValidSymbol<T> wraps a live ISymbol reference. Storing it in a pipeline model retains the entire Compilation in memory — every syntax tree, every symbol table — preventing garbage collection across edits. It also has broken equality semantics.

The Solution

Snapshot types extract all relevant data from symbols into plain record types with correct equality. Use them in pipeline models instead of ValidSymbol<T> or ISymbol.

// ❌ Retains entire Compilation
public required ValidSymbol<INamedTypeSymbol> TargetType { get; init; }

// ✅ Pipeline-safe, correct equality
public required TypeSnapshot TargetType { get; init; }

Snapshot Hierarchy

All snapshots inherit from SymbolSnapshot:

Snapshot Mirrors Key Properties
SymbolSnapshot ValidSymbol<ISymbol> (base) Name, Namespace, FQN, accessibility, modifiers, documentation
TypeSnapshot ValidSymbol<INamedTypeSymbol> Type identity, generics, base type, interfaces
MethodSnapshot ValidSymbol<IMethodSymbol> Return type, async kind, parameters
PropertySnapshot ValidSymbol<IPropertySymbol> Type, getter/setter, init-only, required
FieldSnapshot ValidSymbol<IFieldSymbol> Type, const, volatile, constant value
ParameterSnapshot ValidSymbol<IParameterSymbol> Type, default value, ref kind, params
EventSnapshot ValidSymbol<IEventSymbol> Type

Creating Snapshots

From ValidSymbol (.ToSnapshot())

Every ValidSymbol<T> specialization has a .ToSnapshot() extension:

ValidSymbol<INamedTypeSymbol> symbol = ...;
TypeSnapshot snapshot = symbol.ToSnapshot();

ValidSymbol<IMethodSymbol> method = ...;
MethodSnapshot snapshot = method.ToSnapshot();

From Query Builders (.Snapshots() / .Snapshot())

All query builders have snapshot terminals that materialize results directly:

// Multiple results
EquatableArray<MethodSnapshot> methods = type.QueryMethods()
    .ThatArePublic()
    .ThatAreInstance()
    .Snapshots();

// Single result (first or null)
TypeSnapshot? baseType = compilation.QueryTypes()
    .WithName("MyBase")
    .Snapshot();

Available on all query builders:

Query .Snapshots() returns .Snapshot() returns
TypeQuery EquatableArray<TypeSnapshot> TypeSnapshot?
MethodQuery EquatableArray<MethodSnapshot> MethodSnapshot?
PropertyQuery EquatableArray<PropertySnapshot> PropertySnapshot?
FieldQuery EquatableArray<FieldSnapshot> FieldSnapshot?
ParameterQuery EquatableArray<ParameterSnapshot> ParameterSnapshot?
EventQuery EquatableArray<EventSnapshot> EventSnapshot?
ConstructorQuery EquatableArray<MethodSnapshot> MethodSnapshot?

SymbolSnapshot (Base)

Properties shared by all snapshots:

Name & Display

snapshot.Name                    // "MyClass"
snapshot.Namespace               // "MyApp.Models" (null for global)
snapshot.FullyQualifiedName      // "MyApp.Models.MyClass"
snapshot.GloballyQualifiedName   // "global::MyApp.Models.MyClass"
snapshot.DisplayName             // "MyApp.Models.MyClass" (computed)
snapshot.PropertyName            // "MyClass"
snapshot.ParameterName           // "myClass"

Accessibility

snapshot.AccessibilityString     // "public", "internal", etc.
snapshot.IsPublic                // bool
snapshot.IsInternal              // bool

Modifiers

snapshot.IsStatic
snapshot.IsAbstract
snapshot.IsSealed
snapshot.IsVirtual
snapshot.IsOverride
snapshot.IsReadOnly
snapshot.IsPartial

Type Classification

snapshot.Kind                    // "class", "struct", "interface", etc.
snapshot.IsValueType
snapshot.IsReferenceType
snapshot.IsNullable

Documentation

snapshot.Documentation           // DocumentationSnapshot
snapshot.Documentation.Summary   // "Description of the symbol"
snapshot.Documentation.HasValue  // true if any documentation exists

TypeSnapshot

Extends SymbolSnapshot with type-specific data:

TypeSnapshot type = symbol.ToSnapshot();

// Type identity
type.IsInterface      // bool
type.IsClass          // bool
type.IsStruct         // bool
type.IsRecord         // bool
type.IsEnum           // bool
type.IsDelegate       // bool

// Generics
type.IsGenericType    // bool
type.Arity            // int (number of type parameters)
type.TypeArgumentNames // EquatableArray<string> — FQN of type args

// Hierarchy
type.BaseTypeName     // "global::System.Object" or null
type.InterfaceNames   // EquatableArray<string> — directly implemented

MethodSnapshot

MethodSnapshot method = symbol.ToSnapshot();

method.ReturnType          // "global::System.Threading.Tasks.Task<int>"
method.ReturnsVoid         // bool
method.AsyncKind           // AsyncMethodKind enum
method.AsyncReturnType     // "global::System.Int32" (T from Task<T>)
method.MethodKind          // MethodKind enum
method.IsExtensionMethod   // bool
method.IsGenericMethod     // bool
method.IsPartialMethod     // bool
method.IsAsync             // bool
method.Parameters          // EquatableArray<ParameterSnapshot>

PropertySnapshot

PropertySnapshot prop = symbol.ToSnapshot();

prop.Type          // "global::System.String"
prop.HasGetter     // bool
prop.HasSetter     // bool
prop.IsInitOnly    // bool (init-only setter)
prop.IsRequired    // bool
prop.IsIndexer     // bool
prop.Parameters    // EquatableArray<ParameterSnapshot> (indexer params)

FieldSnapshot

FieldSnapshot field = symbol.ToSnapshot();

field.Type                     // "global::System.Int32"
field.IsConst                  // bool
field.IsVolatile               // bool
field.HasConstantValue         // bool
field.ConstantValueExpression  // "42" or "\"hello\"" (C# expression string)

ParameterSnapshot

ParameterSnapshot param = symbol.ToSnapshot();

param.Type                     // "global::System.String"
param.HasExplicitDefaultValue  // bool
param.DefaultValueExpression   // "\"default\"" (C# expression string)
param.RefKind                  // RefKind enum (None, Ref, Out, In)
param.IsParams                 // bool
param.IsOptional               // bool

EventSnapshot

EventSnapshot evt = symbol.ToSnapshot();

evt.Type    // "global::System.EventHandler"

DocumentationSnapshot

Pipeline-safe replacement for XmlDocumentation:

DocumentationSnapshot docs = symbol.XmlDocumentation.ToSnapshot();

docs.Summary        // "Summary text"
docs.Remarks        // "Additional remarks"
docs.Returns        // "Return value description"
docs.Value          // "Property value description"
docs.Example        // "Example code"
docs.HasValue       // true if any documentation exists
docs.Params         // EquatableArray<ParamDocumentation>
docs.TypeParams     // EquatableArray<ParamDocumentation>
docs.Exceptions     // EquatableArray<ExceptionDocumentation>
docs.SeeAlso        // EquatableArray<string>

Supporting types:

// Parameter documentation
record ParamDocumentation(string Name, string Description);

// Exception documentation
record ExceptionDocumentation(string Type, string Description);

TypeRef Integration

TypeRef.From() accepts snapshots so Emit writers work unchanged:

TypeSnapshot type = ...;
TypeRef typeRef = TypeRef.From(type);  // uses GloballyQualifiedName

Real-World Example

Before (broken caching)

public sealed record CapabilityModel
{
    public required ValidSymbol<INamedTypeSymbol> DependencyType { get; init; }
    public required ImmutableArray<ValidSymbol<IMethodSymbol>> Methods { get; init; }
}

After (correct caching)

[PipelineModel]
public sealed record CapabilityModel
{
    public required TypeSnapshot DependencyType { get; init; }
    public required EquatableArray<MethodSnapshot> Methods { get; init; }
}

Projection code

// Before
DependencyType = attribute.TargetType,
Methods = attribute.TargetType.QueryMethods().GetAll(),

// After
DependencyType = attribute.TargetType.ToSnapshot(),
Methods = attribute.TargetType.QueryMethods().Snapshots(),