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:
- Nested Case Classes - One sealed class per case
- Factory Methods - Static methods to create instances
- Pattern Matching -
MatchandMatchAsyncmethods - Type Checking -
Is{CaseName}properties - Value Extraction -
TryGet{CaseName}methods - Equality Members -
Equals,GetHashCode,==,!= - ToString Override - Human-readable representation
- 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>orTask - 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
trueif the union is in the specified case,falseotherwise outparameter 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
Matchinstead)
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 == nullistrue,null == valueisfalse - 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β
| Operation | Complexity | Allocations |
|---|---|---|
| Factory method | O(1) | 1 object |
| Pattern matching | O(1) | 0 (closures may allocate) |
| Type checking | O(1) | 0 |
| Value extraction | O(1) | 0 |
| Equality | O(n) | 0 |
| GetHashCode | O(n) | 0 |
| ToString | O(n) | String allocation |
Where n = number of fields in the active case.
π Best Practicesβ
- Use Match over type checks - More maintainable and exhaustive
- Prefer immutable data - Reference types in cases should be immutable
- Avoid ToString in hot paths - It allocates strings
- Use TryGet for single-case handling - Cleaner than Match when appropriate
- Leverage async patterns - Use MatchAsync for I/O-bound operations
π Next Stepsβ
- See Pattern Matching Guide for detailed matching patterns
- Review Best Practices for production usage
- Check Attributes Reference for configuration options