Skip to main content

ScottEncodingGenerator by Georgiy Petrov

NuGet / site data

Nuget GitHub last commit GitHub Repo stars

Details

Info

info

Name: ScottEncodingGenerator

Roslyn incremental source generator for partial interfaces and abstract partial classes that generates Match methods, nested case implementations, and companion module helpers for Scott-encoded types.

Author: Georgiy Petrov

NuGet: https://www.nuget.org/packages/ScottEncodingGenerator/

You can find more details at https://github.com/Georgiy-Petrov/ScottEncodingGenerator

Source: https://github.com/Georgiy-Petrov/ScottEncodingGenerator

Author

note

Georgiy Petrov Alt text

Original Readme

note

ScottEncodingGenerator

ScottEncodingGenerator is a C# source generator for defining closed sets of nested case types and generating a Match API for them.

It targets types marked with [ScottEncoding] and generates:

  • a Match<TResult>(...) member on the target type
  • per-case Match implementations on nested case types
  • factory methods for constructing cases
  • step-by-step extension methods for building a full match expression

The generator supports:

  • partial interface targets
  • abstract partial class targets

It does not support:

  • structs as targets
  • generic nested cases
  • non-partial nested cases

NuGet

Purpose

The generator is intended for code shaped like a discriminated union encoded with nested types.

Instead of manually writing:

  • the common Match contract
  • one implementation per case
  • helper factory methods
  • fluent matching helpers

the generator derives those from the target type and its nested case declarations.

Example

######### Input

[ScottEncoding]
public abstract partial class Option<T>
{
public sealed partial class Some
{
public Some(T value) => Value = value;
public T Value \{ get; }
}

public sealed partial class None
{
public None() \{ }
}
}

######### Generated shape

The generator adds a Match member to the target:

public abstract partial class Option<T>
{
public abstract TResult Match<TResult>(
Func<Option<T>.Some, TResult> some,
Func<Option<T>.None, TResult> none);

public sealed partial class Some : Option<T>
{
public override TResult Match<TResult>(
Func<Option<T>.Some, TResult> some,
Func<Option<T>.None, TResult> none)
=> some(this);
}

public sealed partial class None : Option<T>
{
public override TResult Match<TResult>(
Func<Option<T>.Some, TResult> some,
Func<Option<T>.None, TResult> none)
=> none(this);
}
}

It also generates a companion helper type:

public static partial class OptionModule
{
public static Option<T> Some<T>(T value) => new Option<T>.Some(value);
public static Option<T> None<T>() => new Option<T>.None();

public static MatchStep2<T, TResult> IfSome<T, TResult>(
this Option<T> value,
Func<Option<T>.Some, TResult> some)
=> new(value, some);
}

Usage:

Option<int> x = OptionModule.Some(10);

var text = x.Match(
some => $"Some({some.Value})",
none => "None");

Or with the generated step API:

var text = x
.IfSome(some => $"Some({some.Value})")
.IfNone(_ => "None");
Interface targets

The generator also supports interface targets.

######### Input

[ScottEncoding]
public partial interface Expr
{
public sealed partial class Constant
{
public Constant(int value) => Value = value;
public int Value \{ get; }
}

public sealed partial class Add
{
public Add(Expr left, Expr right)
{
Left = left;
Right = right;
}

public Expr Left \{ get; }
public Expr Right \{ get; }
}
}

######### Generated shape

For interfaces, the generator emits:

  • a Match<TResult>(...) declaration on the interface
  • nested case classes implementing the interface
  • helper factories in ExprModule

Example use:

Expr expr = ExprModule.Add(
ExprModule.Constant(1),
ExprModule.Constant(2));

var value = expr.Match(
constant => constant.Value,
add => add.Left.Match(
l => l.Value,
_ => 0) +
add.Right.Match(
r => r.Value,
_ => 0));
Rules

A target type must:

  • be marked with [ScottEncoding]

  • be partial

  • be either:

    • an abstract partial class, or
    • a partial interface

A nested case type must:

  • be declared inside the target
  • be a non-generic class
  • be partial
  • be non-abstract
  • expose at least one accessible instance constructor

For class targets, a nested case may:

  • omit an explicit base type, or
  • inherit from the target type

For interface targets, a nested case may:

  • omit an explicit base type, or
  • implement the target interface

A nested case must not specify an unrelated base type.

Factories

For each accessible constructor on each case, the generator emits a factory method in <TargetName>Module.

Example:

public sealed partial class Error
{
public Error(string message) => Message = message;
public string Message \{ get; }
}

Generates a factory similar to:

public static Result<T> Error<T>(string message) => new Result<T>.Error(message);

If a case has multiple accessible constructors, additional factory methods are emitted with suffixes derived from constructor parameters or an index.

Matching API

The generated Match<TResult>(...) method takes one delegate per case, in declaration order.

For a target with cases:

  • Success
  • Failure
  • Loading

the generated signature is shaped like:

TResult Match<TResult>(
Func<Success, TResult> success,
Func<Failure, TResult> failure,
Func<Loading, TResult> loading);

The fluent step API follows the same order:

value
.IfSuccess(...)
.IfFailure(...)
.IfLoading(...);
Diagnostics

The generator reports the following diagnostics.

######### SCOTT001

Target is not valid.

Raised when the target is not:

  • partial
  • an interface
  • or an abstract class

######### SCOTT002

No valid cases were found.

Raised when the target does not declare any usable nested case types.

######### SCOTT003

A nested case has an invalid shape.

Raised when a case is not a:

  • partial class
  • non-generic class
  • non-abstract class

######### SCOTT004

A nested case has no accessible instance constructor.

######### SCOTT005

A nested case has an invalid relationship to the target.

Raised when a case declares an unrelated base type.

######### SCOTT006

The target already declares a conflicting Match method.

Generated files

The generator emits:

  • ScottEncodingAttribute.g.cs
  • one generated file per valid target, named from the target symbol

The generated code enables nullable references with:

#nullable enable
Notes
  • Cases are processed in a stable order based on syntax location.
  • The helper type is named <TargetName>Module.
  • The helper type is generated as static partial.
  • Matching delegates use fully qualified type names in generated code.
  • The generated API preserves target type parameters and constraint clauses.
Minimal example
[ScottEncoding]
public abstract partial class BoolLike
{
public sealed partial class True
{
public True() \{ }
}

public sealed partial class False
{
public False() \{ }
}
}

Usage:

BoolLike value = BoolLikeModule.True();

var result = value.Match(
@true => 1,
@false => 0);

About

note

Usage

  1. You declare a union-like type as an abstract partial class and annotate it with ScottEncoding.

  2. Inside it, you define each case as a sealed partial nested class (in this sample: Ok and None).

  3. Your business logic returns one of those cases (Ok when save succeeds, None when it does not).

  4. Consumer code can handle results either with normal C# pattern matching or with the generated functional matching API.

  5. What the generator adds

  6. A Match contract on the parent union type, plus per-case implementations that dispatch to the correct handler.

  7. A companion module with factory helpers to construct each case (Ok / None) without directly calling constructors.

  8. A fluent matching flow (IfOk(...).IfNone(...)) built on top of Match.

  9. A generated ScottEncodingAttribute definition, so the attribute is available at compile time.

How to use

Example (source csproj, source files)

This is the CSharp Project that references ScottEncodingGenerator

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ScottEncodingGenerator" Version="1.0.0" />
</ItemGroup>



</Project>

Generated Files

Those are taken from $(BaseIntermediateOutputPath)\GX

// <auto-generated />
#nullable enable

namespace UnionTypesDemo
{
public abstract partial class ResultSave<T>
{
public abstract TResult Match<TResult>(global::System.Func<global::UnionTypesDemo.ResultSave<T>.Ok, TResult> ok, global::System.Func<global::UnionTypesDemo.ResultSave<T>.None, TResult> none);

public sealed partial class Ok : global::UnionTypesDemo.ResultSave<T>
{
public override TResult Match<TResult>(global::System.Func<global::UnionTypesDemo.ResultSave<T>.Ok, TResult> ok, global::System.Func<global::UnionTypesDemo.ResultSave<T>.None, TResult> none) => ok(this);
}

public sealed partial class None : global::UnionTypesDemo.ResultSave<T>
{
public override TResult Match<TResult>(global::System.Func<global::UnionTypesDemo.ResultSave<T>.Ok, TResult> ok, global::System.Func<global::UnionTypesDemo.ResultSave<T>.None, TResult> none) => none(this);
}
}

public static partial class ResultSaveModule
{
public static global::UnionTypesDemo.ResultSave<T> Ok<T>(T value) => new global::UnionTypesDemo.ResultSave<T>.Ok(value);

public static global::UnionTypesDemo.ResultSave<T> None<T>() => new global::UnionTypesDemo.ResultSave<T>.None();

public static MatchStep2<T, TResult> IfOk<T, TResult>(this global::UnionTypesDemo.ResultSave<T> value, global::System.Func<global::UnionTypesDemo.ResultSave<T>.Ok, TResult> ok) => new(value, ok);

public readonly struct MatchStep2<T, TResult>

{

private readonly global::UnionTypesDemo.ResultSave<T> _value;

private readonly global::System.Func<global::UnionTypesDemo.ResultSave<T>.Ok, TResult> _ok;



public MatchStep2(global::UnionTypesDemo.ResultSave<T> value, global::System.Func<global::UnionTypesDemo.ResultSave<T>.Ok, TResult> ok)

{

_value = value;

_ok = ok;

}



public TResult IfNone(global::System.Func<global::UnionTypesDemo.ResultSave<T>.None, TResult> none) => _value.Match(_ok, none);

}
}
}

Useful

Download Example (.NET C#)

Share ScottEncodingGenerator

https://ignatandrei.github.io/RSCG_Examples/v2/docs/ScottEncodingGenerator

Category "FunctionalProgramming" has the following generators:

1 cachesourcegenerator Nuget GitHub Repo stars 2024-02-14

2 dunet Nuget GitHub Repo stars 2023-04-16

3 Dusharp Nuget GitHub Repo stars 2024-09-19

4 Funcky.DiscriminatedUnion Nuget GitHub Repo stars 2024-01-18

5 FunicularSwitch NugetNuget GitHub Repo stars 2024-02-12

6 N.SourceGenerators.UnionTypes Nuget GitHub Repo stars 2023-10-29

7 OneOf NugetNuget GitHub Repo stars 2023-08-21

8 PartiallyApplied Nuget GitHub Repo stars 2023-04-16

9 polytype Nuget GitHub Repo stars 2024-11-04

10 rscg_demeter Nuget GitHub Repo stars 2025-03-26

11 rscg_queryables NugetNuget GitHub Repo stars 2024-11-02

12 RSCG_Utils_Memo Nuget GitHub Repo stars 2023-08-27

13 ScottEncodingGenerator Nuget GitHub Repo stars 2026-06-30

14 Sera.Union Nuget GitHub Repo stars 2024-08-26

15 Sundew.DiscriminatedUnions Nuget GitHub Repo stars 2026-02-14

16 TypeUtilities Nuget GitHub Repo stars 2024-03-05

17 UnionGen Nuget GitHub Repo stars 2024-04-05

18 UnionsGenerator Nuget GitHub Repo stars 2024-02-18

See category

FunctionalProgramming