Skip to content

refactor: Introduce per-type emission strategies #44

@DrBarnabus

Description

@DrBarnabus

Context

The Emitter (SourceGenerator.Emitter.cs) is responsible for generating type-specific C# code for parsing and formatting composite key properties. It uses switch statements on ParseType and FormatType enums to select the correct code generation logic for each supported type (e.g., parsing a Guid vs a string vs an enum).

Currently, these switch statements are duplicated across 4 separate sites in the Emitter, totalling approximately 129 lines of type-dispatching code:

# Method / Site Lines Purpose
1 WriteParsePropertiesImplementation switch Emitter:467-505 (39 lines) Emit parse code for a single property value
2 WriteRepeatingItemParse switch Emitter:593-629 (37 lines) Emit parse code for one item within a repeating/collection property
3 WriteFormatMethodBodyForKeyParts optimised string.Create path Emitter:722-753 (32 lines) Emit span-based formatting using string.Create with exact-length buffer
4 WriteFormatMethodBodyForKeyParts interpolation path Emitter:819-839 (21 lines) Emit formatting using DefaultInterpolatedStringHandler (fallback path)

The ParseType enum defines the parse strategies: Guid, String, Enum, SpanParsable.

The FormatType enum defines the format strategies: Guid, String, Enum, SpanFormattable.

Problem

Adding a new supported type (e.g., DateOnly, int, or a custom type) currently requires modifying all 4 switch sites in the Emitter. Each site has subtly different parameters and code shape, making this error-prone and hard to review. The type-specific logic is scattered rather than cohesive.

Solution

Introduce per-type strategy objects that encapsulate all type-specific code generation behind a common interface. This reduces the "add a new type" change from modifying 4 Emitter switch sites to creating 1 new strategy class. The Emitter dispatch sites collapse from switch statements into single strategy method calls.

All strategies are stateless singletons (static readonly instances) accessed via a static dictionary, ensuring zero allocation overhead at generation time.

Blocked by

Tasks

Define ITypeStrategy interface

  1. Create internal interface ITypeStrategy with four methods corresponding to the 4 switch sites:
    • EmitSingleParse(SourceWriter, inputVar, outputVar, PropertyKeyPart, shouldThrow) — replaces site 1 (single-value parse code)
    • EmitRepeatingItemParse(SourceWriter, inputVar, itemVar, listVar, PropertyKeyPart, shouldThrow) — replaces site 2 (collection item parse code)
    • TryEmitSpanFormat(SourceWriter, PropertyKeyPart, positionVar, invariantFormatting) → bool — replaces site 3 (optimised string.Create path; returns false if type doesn't support this path)
    • EmitInterpolationFormat(SourceWriter, PropertyKeyPart) — replaces site 4 (DefaultInterpolatedStringHandler path)

Implement concrete strategies

  1. Implement four singleton strategies corresponding to each ParseType/FormatType pair:
    • GuidTypeStrategyGuid.TryParseExact for parse, ISpanFormattable.TryFormat for span format, AppendFormatted with format string for interpolation
    • StringTypeStrategy — length-check + ToString() for parse, CopyTo for span format, AppendFormatted for interpolation
    • EnumTypeStrategy — delegates to the generated {EnumName}Helper.TryParse/TryFormat/GetFormattedLength methods for parse and format. Implementation note: this strategy will need to call into EnumGenerationHelper.cs, which generates optimised per-enum helper classes (TryParse, TryFormat, GetFormattedLength) with array-lookup fast paths for sequential-from-zero enums. The strategy emits calls to these helpers rather than containing enum logic directly.
    • SpanParsableTypeStrategyT.TryParse for parse, AppendFormatted for interpolation (span format returns false, falling back to interpolation path)
  2. Each strategy encapsulates the type-specific code currently spread across the 4 switch sites listed above
  3. Strategies accessed via a static dictionary keyed by ParseType (e.g., Dictionary<ParseType, ITypeStrategy>)
  4. All strategies are stateless singletons (static readonly) for zero allocation overhead — no per-call or per-property allocation

Refactor Emitter to use strategies

  1. Replace the 4 switch statement sites with strategy dispatch:
    var strategy = TypeStrategies[propertyKeyPart.ParseType];
    strategy.EmitSingleParse(writer, inputVar, outputVar, propertyKeyPart, shouldThrow);
  2. Single/repeating distinction handled by checking CollectionSemantics on the PropertyKeyPart:
    if (propertyKeyPart.CollectionSemantics is not null)
        strategy.EmitRepeatingItemParse(...)
    else
        strategy.EmitSingleParse(...)

    Note: After refactor: Unify model layer and type resolution #43 (the unified model refactoring), PropertyKeyPart carries a CollectionSemantics? field — null for single properties, non-null for repeating/collection properties. This replaces the current separate RepeatingPropertyKeyPart subclass and is what the strategy dispatch uses to determine which emit method to call.

  3. The optimised string.Create path (site 3) uses TryEmitSpanFormat — if the strategy returns false, the Emitter falls back to the interpolation path automatically

Verification

  • All existing snapshot tests pass with identical generated output — this is a pure refactoring with no behavioural change
  • Run benchmarks to confirm strategy dispatch does not introduce allocations or measurable overhead vs the current inline switches
  • Verify that adding a hypothetical new type requires only creating a new ITypeStrategy implementation and registering it in the dictionary (no Emitter switch modifications)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions