Skip to main content

Generated API Reference

Complete reference for all members generated by UnionGenerator when you mark a class with [GenerateUnion].

πŸ—οΈ What Gets Generated​

For every union type you define, UnionGenerator creates:

  1. Nested Case Classes - One sealed class per case
  2. Factory Methods - Static methods to create instances
  3. Pattern Matching - Match and MatchAsync methods
  4. Type Checking - Is{CaseName} properties
  5. Value Extraction - TryGet{CaseName} methods
  6. Equality Members - Equals, GetHashCode, ==, !=
  7. ToString Override - Human-readable representation
  8. JSON Serialization - Converter attributes (when applicable)

πŸ“¦ Example Union Definition​

using UnionGenerator.Attributes;

[GenerateUnion]
public partial class Result<T, TError>
{
public static partial Result<T, TError> Ok(T value);
public static partial Result<T, TError> Error(TError error);
}

This generates approximately 200+ lines of code providing full functionality.

πŸ”§ Generated Members Reference​

1. Factory Methods​

Factory methods create instances of your union. Each method you declare gets fully implemented.

Signature Pattern​

public static {UnionType} {MethodName}({Parameters})

Example​

// Your declaration
[GenerateUnion]
public partial class PaymentResult
{
public static partial PaymentResult Success(decimal amount, string transactionId);
public static partial PaymentResult Failed(string reason);
public static partial PaymentResult Pending();
}

// Usage
var result1 = PaymentResult.Success(99.99m, "TXN-12345");
var result2 = PaymentResult.Failed("Insufficient funds");
var result3 = PaymentResult.Pending();

Characteristics​

  • Thread-safe: Yes, no shared state
  • Performance: O(1) allocation
  • Null safety: Parameters are validated (non-nullable by default)
  • Immutability: Created instances are immutable

2. Pattern Matching Methods​

Pattern matching is the primary way to work with union values. All cases must be handled.

Match (Synchronous)​

Returns a value by matching all cases.

public TResult Match<TResult>(
Func<Case1Data, TResult> case1Handler,
Func<Case2Data, TResult> case2Handler,
// ... one parameter per case
)

Example:

string message = paymentResult.Match(
success: (amount, txnId) => $"Payment of ${amount} succeeded (ID: {txnId})",
failed: reason => $"Payment failed: {reason}",
pending: () => "Payment is pending"
);

Characteristics:

  • Returns: TResult - The result of the matched handler
  • Performance: O(1) - Single switch statement
  • Thread-safe: Yes, if handlers are thread-safe
  • Exceptions: None from Match itself, handlers may throw

Match (Void/Action)​

Executes side effects without returning a value.

public void Match(
Action<Case1Data> case1Handler,
Action<Case2Data> case2Handler,
// ... one parameter per case
)

Example:

paymentResult.Match(
success: (amount, txnId) => Console.WriteLine($"Success: ${amount}"),
failed: reason => Console.WriteLine($"Failed: {reason}"),
pending: () => Console.WriteLine("Pending...")
);

MatchAsync​

Asynchronous pattern matching for async handlers.

public async Task<TResult> MatchAsync<TResult>(
Func<Case1Data, Task<TResult>> case1Handler,
Func<Case2Data, Task<TResult>> case2Handler,
// ... one parameter per case
)

Example:

var response = await paymentResult.MatchAsync(
success: async (amount, txnId) => await SendSuccessEmail(amount, txnId),
failed: async reason => await LogFailure(reason),
pending: async () => await NotifyPending()
);

Characteristics:

  • Returns: Task<TResult> or Task
  • Performance: O(1) + async overhead
  • Cancellation: Does not accept CancellationToken (pass to handlers)
  • ConfigureAwait: Uses default context

3. Type Checking Properties​

Boolean properties to check which case is active.

Signature Pattern​

public bool Is{CaseName} { get; }

Example​

if (paymentResult.IsSuccess)
{
// Handle success case
}
else if (paymentResult.IsFailed)
{
// Handle failure case
}

Characteristics​

  • Performance: O(1) - Direct field comparison
  • Thread-safe: Yes, field is readonly
  • Exactly one is true: Only one property returns true at a time

Warning: ⚠️ Don't use type checks instead of pattern matching. Pattern matching is safer:

// ❌ Bad: Not exhaustive, easy to forget cases
if (result.IsSuccess) { /* ... */ }
else if (result.IsFailed) { /* ... */ }
// Missing pending case!

// βœ… Good: Compiler enforces all cases
result.Match(
success: /* ... */,
failed: /* ... */,
pending: /* ... */
);

4. Value Extraction Methods​

Extract the data from a specific case if active.

Signature Pattern​

public bool TryGet{CaseName}(out {DataType} value)

Example​

// Single value case
if (paymentResult.TryGetFailed(out string reason))
{
Console.WriteLine($"Failure reason: {reason}");
}

// Multiple values case (uses ValueTuple)
if (paymentResult.TryGetSuccess(out var successData))
{
var (amount, transactionId) = successData;
Console.WriteLine($"Amount: {amount}, TxnId: {transactionId}");
}

// No-value case
if (paymentResult.TryGetPending(out var _))
{
Console.WriteLine("Payment is pending");
}

Return Values​

  • Returns true if the union is in the specified case, false otherwise
  • out parameter contains:
    • The single value (for single-parameter cases)
    • A ValueTuple (for multi-parameter cases)
    • Unit.Value (for parameterless cases)

Characteristics​

  • Performance: O(1) - Type check + cast
  • Thread-safe: Yes
  • Null safety: Out parameter is nullable for reference types

When to use:

  • βœ… When you only care about one specific case
  • βœ… When using pattern matching would be overkill
  • ❌ When you need to handle all cases (use Match instead)

5. Equality Members​

Full structural equality support.

Equals Method​

public override bool Equals(object? obj)
public bool Equals(Result<T, TError>? other) // IEquatable<T>

Behavior:

  • Two unions are equal if they're the same case with equal data
  • Different cases are never equal
  • Uses value equality for data (calls .Equals() on fields)

Example:

var result1 = Result<int, string>.Ok(42);
var result2 = Result<int, string>.Ok(42);
var result3 = Result<int, string>.Ok(99);

Console.WriteLine(result1.Equals(result2)); // True
Console.WriteLine(result1.Equals(result3)); // False

GetHashCode Method​

public override int GetHashCode()

Behavior:

  • Combines case discriminator with data hash codes
  • Consistent with Equals (equal objects have equal hash codes)
  • Suitable for use in hash-based collections

Example:

var dict = new Dictionary<Result<int, string>, string>();
dict[Result<int, string>.Ok(42)] = "Success!";

Equality Operators​

public static bool operator ==(Result<T, TError>? left, Result<T, TError>? right)
public static bool operator !=(Result<T, TError>? left, Result<T, TError>? right)

Example:

var result1 = PaymentResult.Success(100m, "TXN-1");
var result2 = PaymentResult.Success(100m, "TXN-1");

if (result1 == result2)
{
Console.WriteLine("Results are equal");
}

Characteristics​

  • Null handling: null == null is true, null == value is false
  • Performance: O(n) where n is the size of data fields
  • Thread-safe: Yes

6. ToString Method​

Human-readable string representation.

public override string ToString()

Format​

{CaseName} { Field1 = Value1, Field2 = Value2 }

Examples​

var success = PaymentResult.Success(99.99m, "TXN-123");
Console.WriteLine(success);
// Output: Success { amount = 99.99, transactionId = TXN-123 }

var failed = PaymentResult.Failed("Insufficient funds");
Console.WriteLine(failed);
// Output: Failed { reason = Insufficient funds }

var pending = PaymentResult.Pending();
Console.WriteLine(pending);
// Output: Pending { }

Characteristics​

  • Performance: O(n) - Concatenates all field values
  • Localization: Not localized, always uses invariant culture
  • Purpose: Debugging and logging, not for UI display

7. Nested Case Classes​

Each case becomes a sealed nested class implementing the union interface.

Structure​

public sealed class {CaseName}Case : {UnionType}
{
// Fields for case data
public readonly {Type1} {Field1};
public readonly {Type2} {Field2};

// Constructor (internal)
internal {CaseName}Case({Type1} field1, {Type2} field2) { /* ... */ }

// Deconstruct for pattern matching
public void Deconstruct(out {Type1} field1, out {Type2} field2) { /* ... */ }
}

Example​

// For this union:
[GenerateUnion]
public partial class Result<T, TError>
{
public static partial Result<T, TError> Ok(T value);
public static partial Result<T, TError> Error(TError error);
}

// Generated case classes:
public sealed class OkCase : Result<T, TError>
{
public readonly T Value;

internal OkCase(T value) => Value = value;

public void Deconstruct(out T value) => value = Value;
}

public sealed class ErrorCase : Result<T, TError>
{
public readonly TError Error;

internal ErrorCase(TError error) => Error = error;

public void Deconstruct(out TError error) => error = Error;
}

Characteristics​

  • Sealed: Cannot be inherited
  • Internal constructors: Can only be created via factory methods
  • Readonly fields: Immutable after construction
  • Deconstruct support: Works with C# pattern matching

8. Type Casting​

You can cast a union to its specific case type for advanced scenarios.

if (result is Result<int, string>.OkCase okCase)
{
int value = okCase.Value;
Console.WriteLine($"Success value: {value}");
}

When to use:

  • Advanced scenarios with reflection
  • Generic code that works with case types directly
  • Recommended: Use pattern matching instead in normal code

🎯 Generic Type Support​

UnionGenerator fully supports generic types with proper constraint propagation.

Example​

[GenerateUnion]
public partial class Result<T, TError>
where T : notnull
where TError : Exception
{
public static partial Result<T, TError> Ok(T value);
public static partial Result<T, TError> Error(TError error);
}

All constraints are preserved in generated code.

πŸ”’ Thread Safety​

All generated members are thread-safe:

  • βœ… Factory methods create independent instances
  • βœ… Pattern matching is stateless
  • βœ… Type checking reads readonly fields
  • βœ… Equality operations have no side effects

Mutable data warning: If case data is mutable, the union itself doesn't protect against concurrent modifications to that data.

⚑ Performance Summary​

OperationComplexityAllocations
Factory methodO(1)1 object
Pattern matchingO(1)0 (closures may allocate)
Type checkingO(1)0
Value extractionO(1)0
EqualityO(n)0
GetHashCodeO(n)0
ToStringO(n)String allocation

Where n = number of fields in the active case.

πŸ“ Best Practices​

  1. Use Match over type checks - More maintainable and exhaustive
  2. Prefer immutable data - Reference types in cases should be immutable
  3. Avoid ToString in hot paths - It allocates strings
  4. Use TryGet for single-case handling - Cleaner than Match when appropriate
  5. Leverage async patterns - Use MatchAsync for I/O-bound operations

πŸš€ Next Steps​