Basic Concepts
Understanding the core concepts behind UnionGenerator will help you write better, safer code. Let's explore discriminated unions, pattern matching, and how they work together.
What is a Discriminated Union?
A discriminated union (also called a tagged union or sum type) is a type that can be one of several predefined variants, where each variant has:
- A tag (discriminator) that identifies which variant it is
- Associated data specific to that variant
- Type safety that ensures you handle all cases
Think of it like a type-safe "one of these" container:
// Without unions: multiple return types with nullable hell
public class ApiResponse
{
public User? User { get; set; }
public string? Error { get; set; }
public bool IsSuccess { get; set; }
}
// With unions: clear, type-safe alternatives
[GenerateUnion]
public partial record ApiResponse
{
public static partial ApiResponse Success(User user);
public static partial ApiResponse Error(string message);
}
The Power of Pattern Matching
Pattern matching lets you safely extract values from a union based on its current variant. UnionGenerator provides multiple ways to match:
Switch Expression Pattern
The most elegant and type-safe approach:
var result = apiResponse.Match(
success => $"Welcome, {success.User.Name}!",
error => $"Failed: {error.Message}"
);
Traditional Switch Statement
For when you need more complex logic:
switch (apiResponse)
{
case ApiResponse.Success success:
Console.WriteLine($"User: {success.User.Name}");
SaveToDatabase(success.User);
break;
case ApiResponse.Error error:
Console.WriteLine($"Error: {error.Message}");
LogError(error.Message);
break;
}
TryGet Pattern
When you only care about one specific variant:
if (apiResponse.TryGetSuccess(out var success))
{
Console.WriteLine($"Got user: {success.User.Name}");
}
Union Anatomy
Let's break down what UnionGenerator creates for you:
[GenerateUnion]
public partial record PaymentResult
{
public static partial PaymentResult Completed(string transactionId, decimal amount);
public static partial PaymentResult Pending(string orderId);
public static partial PaymentResult Failed(string reason, PaymentError error);
}
Generated Components
UnionGenerator creates several components behind the scenes:
1. Case Classes
Each variant becomes a nested record:
// Generated by UnionGenerator
public partial record PaymentResult
{
public sealed record Completed(string TransactionId, decimal Amount) : PaymentResult;
public sealed record Pending(string OrderId) : PaymentResult;
public sealed record Failed(string Reason, PaymentError Error) : PaymentResult;
}
2. Factory Methods
Type-safe constructors for each variant:
var completed = PaymentResult.Completed("TX-12345", 99.99m);
var pending = PaymentResult.Pending("ORD-67890");
var failed = PaymentResult.Failed("Insufficient funds", PaymentError.InsufficientFunds);
3. Match Methods
Exhaustive pattern matching support:
// Match with return value
var message = payment.Match(
completed => $"Payment {completed.TransactionId} completed: ${completed.Amount}",
pending => $"Order {pending.OrderId} is pending",
failed => $"Payment failed: {failed.Reason}"
);
// Match with side effects
payment.Match(
completed => SendReceipt(completed.TransactionId),
pending => SendPendingNotification(pending.OrderId),
failed => LogPaymentError(failed.Reason, failed.Error)
);
4. TryGet Methods
Safe variant extraction:
if (payment.TryGetCompleted(out var completed))
{
Console.WriteLine($"Transaction ID: {completed.TransactionId}");
Console.WriteLine($"Amount: ${completed.Amount}");
}
5. Is Properties
Quick variant checks:
if (payment.IsCompleted)
{
// Handle completed payment
}
Type Safety Guarantees
UnionGenerator provides compile-time safety in several ways:
Exhaustive Matching
The compiler ensures you handle all cases:
var result = payment.Match(
completed => "Completed",
pending => "Pending"
// ERROR: Missing case for 'Failed'
);
No Null References
Union variants never return null - you always get a valid variant:
// No null checks needed!
var payment = PaymentResult.Completed("TX-123", 50.00m);
// payment is guaranteed to be non-null
Invalid State Prevention
Impossible states become unrepresentable:
// Before: All these states are possible (even invalid ones)
public class PaymentState
{
public bool IsCompleted { get; set; }
public bool IsPending { get; set; }
public bool IsFailed { get; set; }
// What if IsCompleted AND IsFailed are both true? 🤔
}
// After: Only valid states are possible
[GenerateUnion]
public partial record PaymentState
{
public static partial PaymentState Completed();
public static partial PaymentState Pending();
public static partial PaymentState Failed();
// Can only be ONE of these at a time! ✅
}
Common Design Patterns
Result Pattern
Represent success or failure:
[GenerateUnion]
public partial record Result<T, TError>
{
public static partial Result<T, TError> Success(T value);
public static partial Result<T, TError> Failure(TError error);
}
// Usage
public Result<User, string> GetUser(int id)
{
var user = database.FindUser(id);
return user != null
? Result<User, string>.Success(user)
: Result<User, string>.Failure("User not found");
}
Option Pattern
Represent presence or absence:
[GenerateUnion]
public partial record Option<T>
{
public static partial Option<T> Some(T value);
public static partial Option<T> None();
}
// Usage
public Option<User> FindUserByEmail(string email)
{
var user = users.FirstOrDefault(u => u.Email == email);
return user != null
? Option<User>.Some(user)
: Option<User>.None();
}
State Machine Pattern
Model complex state transitions:
[GenerateUnion]
public partial record OrderState
{
public static partial OrderState Draft(List<OrderItem> items);
public static partial OrderState Submitted(DateTime submittedAt);
public static partial OrderState Processing(string workerId);
public static partial OrderState Shipped(string trackingNumber);
public static partial OrderState Delivered(DateTime deliveredAt);
public static partial OrderState Cancelled(string reason);
}
Next Steps
Now that you understand the core concepts, you're ready to:
- Create Your First Union - Step-by-step guide
- Explore Common Patterns - Real-world examples
Key Takeaways
✅ Discriminated unions provide type-safe alternatives to nullable types and boolean flags
✅ Pattern matching ensures you handle all cases exhaustively
✅ UnionGenerator creates all the boilerplate automatically
✅ Compile-time safety prevents invalid states and missing cases
✅ Common patterns like Result and Option are easy to implement