UnionGenerator 0.1.0

dotnet add package UnionGenerator --version 0.1.0
                    
NuGet\Install-Package UnionGenerator -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="UnionGenerator" Version="0.1.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="UnionGenerator" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="UnionGenerator">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add UnionGenerator --version 0.1.0
                    
#r "nuget: UnionGenerator, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package UnionGenerator@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=UnionGenerator&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=UnionGenerator&version=0.1.0
                    
Install as a Cake Tool

UnionGenerator

Eliminate boilerplate discriminated union code with compile-time safety and zero runtime overhead. Use for error handling, multi-case returns, and type-safe state machinesβ€”without exceptions or nullable chains.

πŸš€ Quick Start (2 minutes)

1. Install

dotnet add package UnionGenerator

2. Define Your Union

using UnionGenerator.Attributes;

[GenerateUnion]
public partial class Result<T, E>
{
    public static Result<T, E> Ok(T value) => new OkCase(value);
    public static Result<T, E> Error(E error) => new ErrorCase(error);
}

3. Use It

var result = Result<int, string>.Ok(42);

// Pattern matching
var message = result switch
{
    { IsSuccess: true, Data: var data } => $"Success: {data}",
    { IsSuccess: false, Error: var error } => $"Error: {error}",
};

// Or use Match method
string message = result.Match(
    ok: data => $"Success: {data}",
    error: err => $"Error: {err}"
);

That's it! You have a fully-featured discriminated union with pattern matching support.


❓ Why Discriminated Unions?

The Problem

In C#, returning multiple possible types from a method is cumbersome:

// ❌ Nullable chains (lose type info, hard to reason about)
public User? GetUser(int id) { /* ... */ }
var user = GetUser(1);
if (user?.IsActive ?? false) { /* ... */ }

// ❌ Exceptions for control flow (costly, lossy error context)
public User GetUser(int id) {
    throw new UserNotFoundException("User not found");
}

// ❌ Output parameters (awkward, less composable)
public bool GetUser(int id, out User user, out string error) { /* ... */ }

// ❌ Custom base classes (boilerplate, manual pattern matching)
public abstract class GetUserResult { }
public class Success : GetUserResult { public User Data { get; set; } }
public class NotFound : GetUserResult { public string Message { get; set; } }

Result: Hard to reason about, boilerplate-heavy, error-prone.

The Solution: Discriminated Unions

// βœ… One type, multiple cases, compile-time exhaustiveness checking
[GenerateUnion]
public partial class GetUserResult
{
    public static GetUserResult Success(User data) => new SuccessCase(data);
    public static GetUserResult NotFound(string message) => new NotFoundCase(message);
}

// Usage: Clear intent, composable, type-safe
var result = GetUser(1);
var message = result switch
{
    { IsSuccess: true, Data: var user } => $"Found: {user.Name}",
    { IsNotFound: true, Message: var msg } => $"Error: {msg}",
};

Benefits

Problem Nullable? Exceptions? UnionGenerator?
Multiple return types ❌ ❌ βœ… Type-safe
Non-happy-path handling ❌ Lossy ❌ Costly βœ… Integrated
Compile-time safety ❌ ❌ βœ… Exhaustiveness
Error context βœ… ❌ Lose data βœ… Structured
Composability ❌ Chains βœ… But awkward βœ… Natural
Performance βœ… ❌ Stack unwinding βœ… Zero overhead

πŸ“š Features

βœ… Compile-Time Generated Code

No runtime reflection, no performance overhead. Pure generated C#.

βœ… Pattern Matching Support

Full support for switch expressions and statements with union cases.

βœ… Factory Methods

Define union cases via static factory methods with compile-time validation.

βœ… ProblemDetails Integration

Built-in support for RFC 7807 error responses in ASP.NET Core.

βœ… Discriminated Union

Type-safe unions with automatic case detection and exhaustiveness checking.

βœ… Zero Allocations

Generated code creates minimal allocations, suitable for high-performance paths.


πŸ”§ How It Works

The Generation Pipeline

[GenerateUnion] Attribute
    ↓
Source Generator detects class
    ↓
Analyzes static factory methods
    ↓
Generates union type implementation
    ↓
.g.cs file created at compile time
    ↓
Full IntelliSense + refactoring support

What Gets Generated

For a union type like:

[GenerateUnion]
public partial class Result<T, E>
{
    public static Result<T, E> Ok(T value) => new OkCase(value);
    public static Result<T, E> Error(E error) => new ErrorCase(error);
}

The generator creates:

  1. Case Classes: OkCase and ErrorCase internal classes
  2. Properties:
    • bool IsSuccess β€” discriminant property
    • T Data β€” success value (if ok)
    • E Error β€” error value (if error)
  3. Methods:
    • Match<TResult>(Func<T, TResult> ok, Func<E, TResult> error) β€” pattern matching
    • Match(Action<T> ok, Action<E> error) β€” void pattern matching
  4. Pattern Support: Full switch expression/statement support

Example Generated Code (Simplified)

public partial class Result<T, E>
{
    private readonly object _value;
    private readonly int _caseId;

    public bool IsSuccess => _caseId == 0;
    public T Data => IsSuccess ? (T)_value : default!;
    public E Error => !IsSuccess ? (E)_value : default!;

    private Result(object value, int caseId)
    {
        _value = value;
        _caseId = caseId;
    }

    // Generated internal case classes
    internal sealed class OkCase : Result<T, E>
    {
        public OkCase(T value) : base(value, 0) { }
    }

    internal sealed class ErrorCase : Result<T, E>
    {
        public ErrorCase(E error) : base(error, 1) { }
    }

    // Generated match methods
    public TResult Match<TResult>(
        Func<T, TResult> ok, 
        Func<E, TResult> error) =>
        IsSuccess ? ok(Data) : error(Error);
}

🎯 Core Components

[GenerateUnion] Attribute

[GenerateUnion]
public partial class MyUnion
{
    // Static factory methods define union cases
    public static MyUnion Success(string data) => new SuccessCase(data);
    public static MyUnion Failure(Exception error) => new FailureCase(error);
}

Rules:

  • Class must be partial
  • Must have 2+ static factory methods
  • Factory methods must return instance of the union type
  • Method names become case identifiers
  • Parameter types must match and be unique

Union Type Structure

// What you define
[GenerateUnion]
public partial class Result<T, E>
{
    public static Result<T, E> Ok(T value) => new OkCase(value);
    public static Result<T, E> Error(E error) => new ErrorCase(error);
}

// What you get
public partial class Result<T, E>
{
    public bool IsSuccess { get; }
    public T Data { get; }
    public E Error { get; }
    
    public TResult Match<TResult>(
        Func<T, TResult> ok,
        Func<E, TResult> error) { }
    
    public void Match(
        Action<T> ok,
        Action<E> error) { }
}

Pattern Matching

var result = GetResult();

// Switch expression
var message = result switch
{
    { IsSuccess: true, Data: int n } when n > 0 => $"Positive: {n}",
    { IsSuccess: true, Data: int n } => $"Non-positive: {n}",
    { IsSuccess: false, Error: var err } => $"Error: {err}",
};

// Switch statement
switch (result)
{
    case { IsSuccess: true, Data: var data }:
        Console.WriteLine($"Success: {data}");
        break;
    case { IsSuccess: false, Error: var error }:
        Console.WriteLine($"Error: {error}");
        break;
}

πŸ“‹ Common Patterns

Pattern 1: Result<T, E> for Error Handling

[GenerateUnion]
public partial class Result<T, E>
{
    public static Result<T, E> Ok(T value) => new OkCase(value);
    public static Result<T, E> Error(E error) => new ErrorCase(error);
}

public class User { public int Id { get; set; } public string Name { get; set; } }
public class ErrorInfo { public string Code { get; set; } public string Message { get; set; } }

// Usage
public Result<User, ErrorInfo> GetUser(int id)
{
    if (id <= 0)
        return Result<User, ErrorInfo>.Error(new ErrorInfo 
        { 
            Code = "INVALID_ID", 
            Message = "ID must be positive" 
        });
    
    var user = _userService.FindById(id);
    return user != null 
        ? Result<User, ErrorInfo>.Ok(user)
        : Result<User, ErrorInfo>.Error(new ErrorInfo 
        { 
            Code = "NOT_FOUND", 
            Message = "User not found" 
        });
}

// Consume
var result = GetUser(1);
if (result.IsSuccess)
{
    Console.WriteLine($"Found user: {result.Data.Name}");
}
else
{
    Console.WriteLine($"Error {result.Error.Code}: {result.Error.Message}");
}

Pattern 2: Multiple Success Cases

[GenerateUnion]
public partial class ParseResult
{
    public static ParseResult Integer(int value) => new IntegerCase(value);
    public static ParseResult Float(double value) => new FloatCase(value);
    public static ParseResult Error(string message) => new ErrorCase(message);
}

// Usage
public ParseResult ParseNumber(string input)
{
    if (int.TryParse(input, out var intValue))
        return ParseResult.Integer(intValue);
    
    if (double.TryParse(input, out var doubleValue))
        return ParseResult.Float(doubleValue);
    
    return ParseResult.Error("Invalid number format");
}

// Consume
var result = ParseNumber("42.5");
result.Match(
    intValue: i => Console.WriteLine($"Integer: {i}"),
    floatValue: d => Console.WriteLine($"Float: {d}"),
    error: e => Console.WriteLine($"Error: {e}")
);

Pattern 3: Option<T> Pattern (Maybe Monad)

[GenerateUnion]
public partial class Option<T>
{
    public static Option<T> Some(T value) => new SomeCase(value);
    public static Option<T> None() => new NoneCase();
}

public class NoneCase
{
    // Parameterless factory for None case
}

// Usage
public Option<User> FindUser(int id)
{
    var user = _service.FindById(id);
    return user != null ? Option<User>.Some(user) : Option<User>.None();
}

// Consume
var option = FindUser(1);
option.Match(
    some: user => Console.WriteLine($"Found: {user.Name}"),
    none: () => Console.WriteLine("User not found")
);

πŸ” Diagnostics & Compile-Time Analysis

The generator reports helpful diagnostics for common mistakes:

UG0001: Invalid Union Type

Severity: Error

Union type must be partial and have at least 2 static factory methods.

// ❌ Error: Not partial
[GenerateUnion]
public class Result { }

// ❌ Error: Only 1 factory method
[GenerateUnion]
public partial class Result
{
    public static Result Ok(int v) => new OkCase(v);
}

// βœ… Correct
[GenerateUnion]
public partial class Result<T, E>
{
    public static Result<T, E> Ok(T v) => new OkCase(v);
    public static Result<T, E> Error(E e) => new ErrorCase(e);
}

UG0002: Factory Parameter Count Mismatch

Severity: Warning

Factory methods must have exactly 1 parameter (the case data).

// ❌ Warning: 2 parameters
public static Result Create(int id, string name) => ...

// βœ… Correct: 1 parameter
public static Result Ok(int value) => ...

// βœ… OK: 0 parameters for "empty" cases
public static Result None() => ...

UG0003: Factory Return Type Mismatch

Severity: Error

All factory methods must return the union type.

// ❌ Error: Returns object instead of Result
public static object Ok(int value) => ...

// βœ… Correct
public static Result Ok(int value) => ...

⚑ Performance

Benchmarks

Operation Time Notes
Create union instance ~5-15 ns Direct constructor
Pattern match (switch) ~10-20 ns Zero overhead vs if/else
Pattern match (Match method) ~15-30 ns Delegate call cost
Property access ~5 ns Direct field read

Optimization Tips

  1. Use switch expressions instead of Match for hot paths
  2. Union types are value types internally (struct-like behavior)
  3. Avoid repeated property access in tight loops
  4. Generics are monomorphized at compile time (no boxing)

πŸ› οΈ Advanced Configuration

Custom Factory Names

[GenerateUnion]
public partial class ApiResponse<T>
{
    public static ApiResponse<T> Success(T data) => new SuccessCase(data);
    public static ApiResponse<T> Failure(string reason) => new FailureCase(reason);
    public static ApiResponse<T> Pending() => new PendingCase();
}

// Usage
var response = ApiResponse<User>.Success(user);
var response = ApiResponse<User>.Failure("Timeout");
var response = ApiResponse<User>.Pending();

Nested Unions

[GenerateUnion]
public partial class Outer<A, B>
{
    public static Outer<A, B> First(A value) => new FirstCase(value);
    public static Outer<A, B> Second(B value) => new SecondCase(value);
}

[GenerateUnion]
public partial class Inner
{
    public static Inner Ok(string data) => new OkCase(data);
    public static Inner Error(string message) => new ErrorCase(message);
}

// Combine them
var nested = Outer<Inner, int>.First(Inner.Ok("success"));

nested.Match(
    first: inner => inner.Match(
        ok: data => Console.WriteLine($"Nested ok: {data}"),
        error: err => Console.WriteLine($"Nested error: {err}")
    ),
    second: num => Console.WriteLine($"Second: {num}")
);

πŸ“– Best Practices

βœ… DO

  • Use Result<T, E> pattern for operations that can fail
  • Define error types as immutable records or classes
  • Use descriptive factory method names (Ok, Error, None, Some, etc.)
  • Leverage pattern matching for exhaustive case handling
  • Keep union types focused (2-4 cases is typical)

❌ DON'T

  • Inherit from union types (they're sealed)
  • Mutate union instances (treat as immutable)
  • Use empty string as None value (use Option<T> instead)
  • Catch exceptions from pattern matching (use Result<T,E> for errors)
  • Create unions with more than 5-6 cases (refactor into smaller unions)

  • UnionGenerator.AspNetCore: ASP.NET Core integration with convention-based HTTP status code mapping
  • UnionGenerator.EntityFrameworkCore: Entity Framework Core value converters for storing Result types
  • UnionGenerator.FluentValidation: FluentValidation integration for Result<T, ValidationError>
  • UnionGenerator.Analyzers: Compile-time diagnostics for union usage in ASP.NET Core
  • UnionGenerator.OneOfCompat: OneOf library compatibility helpers
  • UnionGenerator.OneOfExtensions: OneOf v3 runtime adapters

πŸš€ Getting Started

  1. Install: dotnet add package UnionGenerator
  2. Define: Add [GenerateUnion] to your class
  3. Implement: Add 2+ static factory methods
  4. Use: Access generated properties and methods
  5. Test: Write tests for all union cases

For ASP.NET Core integration, also install:

dotnet add package UnionGenerator.AspNetCore

πŸ§ͺ Testing

[Fact]
public void CreateOkResult_HasIsSuccessTrue()
{
    var result = Result<int, string>.Ok(42);
    
    Assert.True(result.IsSuccess);
    Assert.Equal(42, result.Data);
}

[Fact]
public void CreateErrorResult_HasIsSuccessFalse()
{
    var result = Result<int, string>.Error("Something went wrong");
    
    Assert.False(result.IsSuccess);
    Assert.Equal("Something went wrong", result.Error);
}

[Fact]
public void PatternMatching_HandlesAllCases()
{
    var success = Result<int, string>.Ok(42);
    var error = Result<int, string>.Error("Failed");
    
    var successMsg = success.Match(
        ok: v => $"Ok: {v}",
        error: e => $"Error: {e}"
    );
    
    var errorMsg = error.Match(
        ok: v => $"Ok: {v}",
        error: e => $"Error: {e}"
    );
    
    Assert.Equal("Ok: 42", successMsg);
    Assert.Equal("Error: Failed", errorMsg);
}

πŸ“Š Architecture Overview

UnionGenerator (Core)
β”œβ”€β”€ [GenerateUnion] Attribute
β”œβ”€β”€ Source Generator (ISourceGenerator)
β”‚   β”œβ”€β”€ Syntax Receiver
β”‚   β”œβ”€β”€ Union Type Analyzer
β”‚   └── Code Emitter
β”œβ”€β”€ Union Runtime Support
β”‚   β”œβ”€β”€ Base infrastructure (minimal)
β”‚   └── Match methods
└── Diagnostics
    β”œβ”€β”€ UG0001–UG0003 (Core)
    └── UG4010–UG4012 (AspNetCore, via separate package)

UnionGenerator.Analyzers
β”œβ”€β”€ Roslyn Analyzers (UG4010, UG4011, UG4012)
β”œβ”€β”€ Diagnostic Rules
└── Configuration

UnionGenerator.AspNetCore
β”œβ”€β”€ Convention-based Status Code Mapping
β”œβ”€β”€ HTTP Result Mapping
└── Logging Integration

πŸ› Troubleshooting

Generated Code Not Appearing

Problem: IntelliSense doesn't show generated members

Solution:

  1. Rebuild project: dotnet clean && dotnet build
  2. Check class is marked partial
  3. Ensure attribute is [GenerateUnion] (not misspelled)
  4. Check generated files in obj/Debug/net*/generated/

Factory Method Not Recognized

Problem: Generator reports "Expected factory method not found"

Solution:

  1. Factory must be static
  2. Must return instance of union type
  3. Must have exactly 1 parameter (or 0 for unit cases)
  4. Return type must match union type exactly

Pattern Matching Not Working

Problem: Switch expression doesn't recognize union cases

Solution:

  1. Rebuild project
  2. Ensure IsSuccess, Data, Error properties are accessible
  3. Use correct property names in patterns
  4. Check C# language version (11+ recommended)

⚠️ When NOT to Use

UnionGenerator is powerful but not a fit for every scenario. Consider alternatives if:

1. Only One Success Case + One Error Case

  • Example: bool TryGetUser(int id, out User user, out string error)
  • Better: Use User? GetUser(int id) (nullable) or throw exceptions for truly exceptional cases
  • Why: Nullable T is simpler cognitive load; unions shine with 3+ cases

2. Very Frequently Thrown Exceptions

  • Example: Parsing millions of JSON objects where 90% fail
  • Better: Use Try-pattern (bool TryParse()) or keep exceptionsβ€”they're appropriate here
  • Why: Even zero-overhead unions add cognitive overhead; exceptions are optimized for rare failure paths here

3. Domain Objects with 30+ Internal States

  • Example: Order with state machine (Pending β†’ Processing β†’ Shipped β†’ Delivered β†’ Returned)
  • Better: Use dedicated state machine library (Stateless, NStateMachine) or enum + separate validation logic
  • Why: Too many cases become hard to reason about and pattern match exhaustively

4. Simple Wrapper Types

  • Example: Result<T> { Ok(T), Err(string) } where you only check IsOk property
  • Better: Use (bool Success, T? Data, string? Error) tuple or lightweight wrapper
  • Why: Overhead not justified; simpler types easier to reason about

5. Prototyping / MVP

  • Example: "Let me add discriminated unions before we know if we need error handling"
  • Better: Start with exceptions or nullable; refactor to unions when pattern becomes clear
  • Why: Premature abstraction; let the code tell you when unions help

πŸ“„ License

MIT License - See LICENSE file for details


✨ Summary

Feature Benefit
Compile-Time Generation Zero runtime overhead
Type-Safe Unions Exhaustive pattern matching
Factory Methods Intuitive API surface
Zero Allocations High-performance paths
IDE Support Full IntelliSense integration

Get started now: Mark your class with [GenerateUnion], add factory methods, and enjoy type-safe discriminated unions! πŸš€

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

  • .NETStandard 2.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on UnionGenerator:

Package Downloads
UnionGenerator.OneOfExtensions

Fluent extension methods for converting OneOf types to UnionGenerator unions. Includes JSON serialization helpers with Newtonsoft.Json support.

UnionGenerator.OneOfCompat

Runtime compatibility helpers for converting OneOf library types to UnionGenerator unions. Lightweight reflection-based interoperability for gradual migration.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 166 1/21/2026

Initial release: Core discriminated union source generator with pattern matching support, type-safe error handling, and zero runtime overhead.