Skip to main content

Quick Start

Get up and running with UnionGenerator in 5 minutes. This guide will walk you through creating your first discriminated union.

Prerequisites

  • .NET 6.0 SDK or later
  • Your favorite IDE (Visual Studio, Rider, or VS Code)
  • Basic C# knowledge

Step 1: Create a New Project

dotnet new console -n UnionDemo
cd UnionDemo

Step 2: Install UnionGenerator

dotnet add package UnionGenerator

Step 3: Define Your First Union

Create a Result.cs file:

using UnionGenerator.Attributes;

namespace UnionDemo;

[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);
}

What just happened?

  • [GenerateUnion] tells the source generator to create a discriminated union
  • partial class allows the generator to add code to your class
  • Two static methods define the union cases: Ok and Error
  • The generator creates OkCase and ErrorCase classes automatically

Step 4: Use Your Union

Update Program.cs:

using UnionDemo;

// Create a success result
var success = Result<int, string>.Ok(42);

// Create an error result
var error = Result<int, string>.Error("Something went wrong");

// Pattern matching with Match method
var message1 = success.Match(
ok: value => $"Success: {value}",
error: err => $"Error: {err}"
);
Console.WriteLine(message1); // Output: Success: 42

var message2 = error.Match(
ok: value => $"Success: {value}",
error: err => $"Error: {err}"
);
Console.WriteLine(message2); // Output: Error: Something went wrong

// Pattern matching with switch expression
var message3 = success switch
{
{ IsOk: true, Ok: var value } => $"Got value: {value}",
{ IsError: true, Error: var err } => $"Got error: {err}",
_ => throw new UnreachableException()
};
Console.WriteLine(message3); // Output: Got value: 42

// Check which case is active
if (success.IsOk)
{
Console.WriteLine($"The value is: {success.Ok}");
}

// Try to get a value safely
if (error.TryGetError(out var errorMessage))
{
Console.WriteLine($"Error occurred: {errorMessage}");
}

Step 5: Run It!

dotnet run

Output:

Success: 42
Error: Something went wrong
Got value: 42
The value is: 42
Error occurred: Something went wrong

Real-World Example: HTTP Client

Let's build something more practical—a safe HTTP client:

using System.Net.Http;
using UnionGenerator.Attributes;

[GenerateUnion]
public partial class HttpResult<T>
{
public static HttpResult<T> Success(T data) => new SuccessCase(data);
public static HttpResult<T> NetworkError(string message) => new NetworkErrorCase(message);
public static HttpResult<T> NotFound() => new NotFoundCase();
public static HttpResult<T> Unauthorized() => new UnauthorizedCase();
public static HttpResult<T> ServerError(int statusCode, string message)
=> new ServerErrorCase(statusCode, message);
}

public class ApiClient
{
private readonly HttpClient _client = new();

public async Task<HttpResult<string>> GetAsync(string url)
{
try
{
var response = await _client.GetAsync(url);

return response.StatusCode switch
{
System.Net.HttpStatusCode.OK =>
HttpResult<string>.Success(await response.Content.ReadAsStringAsync()),

System.Net.HttpStatusCode.NotFound =>
HttpResult<string>.NotFound(),

System.Net.HttpStatusCode.Unauthorized =>
HttpResult<string>.Unauthorized(),

_ => HttpResult<string>.ServerError(
(int)response.StatusCode,
response.ReasonPhrase ?? "Unknown error"
)
};
}
catch (HttpRequestException ex)
{
return HttpResult<string>.NetworkError(ex.Message);
}
}
}

// Usage
var client = new ApiClient();
var result = await client.GetAsync("https://api.example.com/data");

var output = result.Match(
success: data => $"Got data: {data}",
networkError: msg => $"Network error: {msg}",
notFound: () => "Resource not found",
unauthorized: () => "Access denied",
serverError: (code, msg) => $"Server error {code}: {msg}"
);

Console.WriteLine(output);

What's Generated?

UnionGenerator creates the following for you:

Case Classes

// Generated by UnionGenerator
public sealed class OkCase : Result<T, E>
{
public T Value { get; }

public OkCase(T value)
{
Value = value;
}
}

public sealed class ErrorCase : Result<T, E>
{
public E Value { get; }

public ErrorCase(E value)
{
Value = value;
}
}

Properties

// Check which case is active
public bool IsOk { get; }
public bool IsError { get; }

// Access case values (throws if wrong case)
public T Ok { get; }
public E Error { get; }

Match Methods

// Synchronous match
public TResult Match<TResult>(
Func<T, TResult> ok,
Func<E, TResult> error
);

// Asynchronous match
public Task<TResult> MatchAsync<TResult>(
Func<T, Task<TResult>> ok,
Func<E, Task<TResult>> error
);

// Action match (no return value)
public void Match(
Action<T> ok,
Action<E> error
);

Try Methods

// Safe value extraction
public bool TryGetOk(out T value);
public bool TryGetError(out E value);

Equality & ToString

public override bool Equals(object? obj);
public override int GetHashCode();
public override string ToString();

Key Concepts

1. One Active Case

Only one case can be active at a time. The type system guarantees this:

var result = Result<int, string>.Ok(42);

Console.WriteLine(result.IsOk); // true
Console.WriteLine(result.IsError); // false

// result.Ok = 100; // ❌ Compile error: no setter

2. Exhaustive Handling

The Match method forces you to handle all cases:

// ✅ All cases handled
var value = result.Match(
ok: v => v,
error: e => 0
);

// ❌ Compile error if you forget a case
var value = result.Match(
ok: v => v
// Missing error parameter!
);

3. Type Safety

Invalid access throws an exception:

var result = Result<int, string>.Error("Failed");

try
{
var value = result.Ok; // ❌ Throws InvalidOperationException
}
catch (InvalidOperationException ex)
{
Console.WriteLine("Tried to access wrong case!");
}

// ✅ Always use TryGet or check first
if (result.IsOk)
{
var value = result.Ok; // Safe!
}

Next Steps

Now that you've created your first union:

  1. Learn core concepts in depth
  2. Build more complex unions
  3. Explore common patterns

Common Questions

Q: Do I need to write CaseACase and CaseBCase classes myself?
A: No! UnionGenerator creates them automatically. You just define the factory methods.

Q: Can I use interfaces or base classes?
A: Yes! Your union class can implement interfaces and the generated cases will too.

Q: What about async operations?
A: Use MatchAsync for async workflows.

Q: How do I serialize unions to JSON?
A: Use System.Text.Json with the included converter.

Q: Can unions have more than 2 cases?
A: Absolutely! You can have as many cases as you need. Just add more static factory methods.

Ready to dive deeper? Continue to Basic Concepts