Key Features
UnionGenerator provides a comprehensive set of features that make discriminated unions powerful and ergonomic in C#.
π Core Featuresβ
Automatic Code Generationβ
Define unions with minimal codeβUnionGenerator handles the rest:
[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);
}
// Generated automatically:
// - OkCase and ErrorCase classes
// - IsOk, IsError properties
// - Match() methods
// - TryGetOk(), TryGetError() methods
// - Equality, GetHashCode, ToString
// - And more...
Exhaustive Pattern Matchingβ
The compiler ensures you handle all cases:
// β
Switch expressions
var message = result switch
{
{ IsOk: true, Ok: var value } => $"Success: {value}",
{ IsError: true, Error: var err } => $"Error: {err}",
_ => throw new UnreachableException() // Never happens
};
// β
Match method
var output = result.Match(
ok: value => ProcessSuccess(value),
error: err => HandleError(err)
);
// β
Async Match
var output = await result.MatchAsync(
ok: async value => await SaveAsync(value),
error: async err => await LogErrorAsync(err)
);
Type-Safe Case Accessβ
Access union cases without casting:
// β
Safe extraction with TryGet
if (result.TryGetOk(out var value))
{
Console.WriteLine($"Got value: {value}");
}
// β
Direct property access (throws if wrong case)
var value = result.Ok; // Throws if IsError
// β
Safe property access
var message = result.IsOk
? $"Success: {result.Ok}"
: $"Error: {result.Error}";
Generic Supportβ
Full support for generic unions:
[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);
}
// Use with any types
Result<int, string> intResult = Result.Ok(42);
Result<User, ValidationError> userResult = Result.Ok(user);
Result<List<Product>, DatabaseError> productsResult = Result.Ok(products);
// Constraints work too
[GenerateUnion]
public partial class Result<T, E> where E : Exception
{
// ...
}
Nested Unionsβ
Unions can contain other unions:
[GenerateUnion]
public partial class ValidationResult<T>
{
public static ValidationResult<T> Valid(T value) => new ValidCase(value);
public static ValidationResult<T> Invalid(ValidationErrors errors) => new InvalidCase(errors);
}
[GenerateUnion]
public partial class ApiResult<T>
{
public static ApiResult<T> Success(T data) => new SuccessCase(data);
public static ApiResult<T> ValidationFailed(ValidationResult<T> validation) => new ValidationFailedCase(validation);
public static ApiResult<T> ServerError(string message) => new ServerErrorCase(message);
}
// Nested matching
return apiResult.Match(
success: data => Ok(data),
validationFailed: validation => validation.Match(
valid: _ => Ok(),
invalid: errors => BadRequest(errors)
),
serverError: msg => StatusCode(500, msg)
);
π Integration Featuresβ
ASP.NET Core Integrationβ
Automatic ProblemDetails mapping:
// Install: dotnet add package UnionGenerator.AspNetCore
[GenerateUnion]
[MapToProblemDetails] // β Automatic HTTP status mapping
public partial class ApiError
{
[StatusCode(404)]
public static ApiError NotFound(string resource) => new NotFoundCase(resource);
[StatusCode(400)]
public static ApiError BadRequest(string message) => new BadRequestCase(message);
[StatusCode(500)]
public static ApiError ServerError(string message) => new ServerErrorCase(message);
}
// In controller
public IActionResult GetUser(int id)
{
var result = _service.GetUser(id);
return result.ToActionResult(); // Automatic ProblemDetails!
}
Entity Framework Core Integrationβ
Store unions in databases:
// Install: dotnet add package UnionGenerator.EntityFrameworkCore
public class Order
{
public int Id { get; set; }
// Union stored as JSON or discriminator
public OrderStatus Status { get; set; }
}
// DbContext configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.Property(o => o.Status)
.HasUnionConversion(); // Automatic value converter!
}
// Queries work naturally
var pendingOrders = await _db.Orders
.Where(o => o.Status.IsPending)
.ToListAsync();
FluentValidation Integrationβ
Map validation errors to unions:
// Install: dotnet add package UnionGenerator.FluentValidation
[GenerateUnion]
public partial class CreateUserResult
{
public static CreateUserResult Success(User user) => new SuccessCase(user);
public static CreateUserResult ValidationFailed(ValidationErrors errors) => new ValidationFailedCase(errors);
}
// Automatic validation to union mapping
public async Task<CreateUserResult> CreateUser(CreateUserCommand command)
{
var validation = await _validator.ValidateAsync(command);
if (!validation.IsValid)
return CreateUserResult.ValidationFailed(validation.ToErrors());
var user = await _repository.CreateAsync(command);
return CreateUserResult.Success(user);
}
OneOf Migration Helperβ
Migrate from OneOf library:
// Install: dotnet add package UnionGenerator.OneOfCompat
// Your old OneOf code
OneOf<Success, NotFound, Error> result = new Success(data);
// UnionGenerator equivalent
[GenerateUnion]
[OneOfCompatible] // β Generates OneOf-style API
public partial class Result<T0, T1, T2>
{
public static Result<T0, T1, T2> FromT0(T0 value) => new T0Case(value);
public static Result<T0, T1, T2> FromT1(T1 value) => new T1Case(value);
public static Result<T0, T1, T2> FromT2(T2 value) => new T2Case(value);
}
π οΈ Developer Experience Featuresβ
Full IntelliSense Supportβ
Generated code works seamlessly with your IDE:
- β Autocomplete for all cases
- β Go to definition
- β Find all references
- β Refactoring support
- β Debugging with F11 step-into
Source Code Visibilityβ
View generated code anytime:
// In Visual Studio/Rider:
// Right-click on [GenerateUnion] β View Generated Code
// Or find it in:
// obj/Debug/net8.0/generated/UnionGenerator/...
Compile-Time Diagnosticsβ
Helpful error messages guide you:
[GenerateUnion]
public partial class Result<T> // β Error: Must have at least 2 factory methods
{
public static Result<T> Ok(T value) => new OkCase(value);
// Missing second case!
}
// Compiler error:
// UG001: Union type 'Result<T>' must define at least two static factory methods
Roslyn Analyzersβ
Optional analyzers catch common mistakes:
// Install: dotnet add package UnionGenerator.Analyzers
var result = GetResult();
// β οΈ Warning: Pattern matching not exhaustive
var value = result switch
{
{ IsOk: true } => result.Ok
// Missing Error case!
};
// β οΈ Warning: Unnecessary null check
if (result.Ok != null) // Result.Ok is never null
{
// ...
}
β‘ Performance Featuresβ
Zero Runtime Overheadβ
Everything compiles to simple field access:
// Your code
var result = Result.Ok(42);
if (result.IsOk)
{
Console.WriteLine(result.Ok);
}
// Compiles to (simplified):
var result = new Result<int, string>.OkCase(42);
if (result._tag == 0) // Simple int comparison
{
Console.WriteLine(result._value); // Direct field access
}
Struct-Based Unionsβ
Generate struct unions for stack allocation:
[GenerateUnion(AsStruct = true)] // β Value type union
public partial struct Option<T>
{
public static Option<T> Some(T value) => new SomeCase(value);
public static Option<T> None() => new NoneCase();
}
// No heap allocations!
Option<int> value = Option.Some(42);
No Reflectionβ
Source generation means zero reflection at runtime:
- β AOT (Native AOT) compatible
- β Trim-friendly
- β Fast startup
- β Small binary size
π¨ Code Quality Featuresβ
Immutability by Defaultβ
Generated types are immutable:
var result = Result.Ok(42);
// result.Ok = 100; // β Compile error: no setter
// Use records for mutable data if needed
public record UserData(string Name, int Age);
Equality and Comparisonβ
Proper value equality:
var result1 = Result.Ok(42);
var result2 = Result.Ok(42);
result1 == result2; // β
true (value equality)
result1.Equals(result2); // β
true
result1.GetHashCode() == result2.GetHashCode(); // β
true
Meaningful ToString()β
Useful debugging output:
var result = Result.Ok(42);
Console.WriteLine(result);
// Output: Result<Int32, String> { Ok = 42 }
var error = Result.Error("Failed");
Console.WriteLine(error);
// Output: Result<Int32, String> { Error = "Failed" }
Next Stepsβ
Ready to get started?