UnionGenerator 0.1.0
dotnet add package UnionGenerator --version 0.1.0
NuGet\Install-Package UnionGenerator -Version 0.1.0
<PackageReference Include="UnionGenerator" Version="0.1.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="UnionGenerator" Version="0.1.0" />
<PackageReference Include="UnionGenerator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add UnionGenerator --version 0.1.0
#r "nuget: UnionGenerator, 0.1.0"
#:package UnionGenerator@0.1.0
#addin nuget:?package=UnionGenerator&version=0.1.0
#tool nuget:?package=UnionGenerator&version=0.1.0
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:
- Case Classes:
OkCaseandErrorCaseinternal classes - Properties:
bool IsSuccessβ discriminant propertyT Dataβ success value (if ok)E Errorβ error value (if error)
- Methods:
Match<TResult>(Func<T, TResult> ok, Func<E, TResult> error)β pattern matchingMatch(Action<T> ok, Action<E> error)β void pattern matching
- 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
- Use switch expressions instead of Match for hot paths
- Union types are value types internally (struct-like behavior)
- Avoid repeated property access in tight loops
- 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)
π Related Packages
- 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
- Install:
dotnet add package UnionGenerator - Define: Add
[GenerateUnion]to your class - Implement: Add 2+ static factory methods
- Use: Access generated properties and methods
- 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:
- Rebuild project:
dotnet clean && dotnet build - Check class is marked
partial - Ensure attribute is
[GenerateUnion](not misspelled) - Check generated files in
obj/Debug/net*/generated/
Factory Method Not Recognized
Problem: Generator reports "Expected factory method not found"
Solution:
- Factory must be
static - Must return instance of union type
- Must have exactly 1 parameter (or 0 for unit cases)
- Return type must match union type exactly
Pattern Matching Not Working
Problem: Switch expression doesn't recognize union cases
Solution:
- Rebuild project
- Ensure
IsSuccess,Data,Errorproperties are accessible - Use correct property names in patterns
- 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 checkIsOkproperty - 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! π
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.