Skip to main content

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:

  1. A tag (discriminator) that identifies which variant it is
  2. Associated data specific to that variant
  3. 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:

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