High-performance discriminated unions for C# - zero-allocation readonly struct core, source generator with class-based named types.
High-performance discriminated unions for C# with exhaustive matching, TryGet patterns, full value equality semantics, a Roslyn incremental source generator for named union types and 39 ready-to-use sentinel and value-carrying types.
- Why Unio?
- Packages
- Installation
- Quick Start
- Supported Arities
- API Reference
- Unio.Types - Pre-built Sentinel & Value Types
- Source Generator
- Real-World Examples
- Performance
- Acknowledgements
- Code Generation Tool
- Building & Testing
- Project Structure
- License
Discriminated unions are a powerful pattern for modeling mutually exclusive states without exceptions or null. C# does not yet have native union types, so developers rely on libraries to fill that gap - but existing solutions often use classes and object boxing, adding GC pressure and losing type safety at runtime.
Unio provides a modern approach:
- Typed generic fields - no casts to
object, no boxing - Exhaustive matching -
Match<TResult>andSwitchforce handling of all cases - Allocation-free matching -
Match<TState, TResult>,Switch<TState>,MatchAsync<TState, TResult>andSwitchAsync<TState>pass context via a state parameter instead of a capturing closure, eliminating lambda allocation on hot paths - Safe access -
TryGetT0..TryGetTnpattern prevents runtime exceptions - Full value equality -
IEquatable<T>,==,!=,GetHashCode - Source generator - define named unions like
StringOrIntwith zero boilerplate - Pre-built types - 39 sentinel and value types for common patterns (NotFound, Success, Error, etc.)
- Maximum performance -
readonly structcore type eliminates heap allocation;[AggressiveInlining]on all hot paths, TieredPGO/DynamicPGO enabled
# Core library (required)
dotnet add package Unio
# Source generator for named union types (optional)
dotnet add package Unio.SourceGenerator
# Pre-built sentinel & value types (optional)
dotnet add package Unio.Types
# ASP.NET Core Minimal API integration (optional)
dotnet add package Unio.AspNetCoreusing Unio;
// Create via implicit conversion - the compiler picks the right slot
Unio<int, string> result = 42;
Unio<int, string> error = "Something went wrong";
// Exhaustive pattern matching - the compiler ensures all cases are handled
string message = result.Match(
value => $"Success: {value}",
err => $"Error: {err}");
// Type checking via IsT# properties
if (result.IsT0)
Console.WriteLine($"Got int: {result.AsT0}");
// Safe access with TryGet - no exceptions thrown
if (result.TryGetT0(out int number))
Console.WriteLine($"Number: {number}");
// Side-effect switching
result.Switch(
value => Console.WriteLine($"Value: {value}"),
err => Console.Error.WriteLine($"Error: {err}"));Create strongly-typed, named union types with zero boilerplate. Install Unio.SourceGenerator, then:
using Unio;
[GenerateUnio]
public partial class StringOrInt : UnioBase<string, int>;
[GenerateUnio]
public partial class ApiResult : UnioBase<User, NotFound, ValidationError>;The source generator produces the full union API automatically:
StringOrInt value = "hello";
if (value.IsT0)
Console.WriteLine(value.AsT0); // "hello"
string result = value.Match(
s => $"string: {s}",
i => $"int: {i}");
// Equality works out of the box
StringOrInt a = 42;
StringOrInt b = 42;
bool equal = a == b; // true
// TryGet, Switch - everything is generated
if (value.TryGetT0(out string str))
Console.WriteLine(str);Install Unio.Types for ready-to-use sentinel types:
using Unio;
using Unio.Types;
// API result pattern with pre-built markers
Unio<User, NotFound, Forbidden> GetUser(int id)
{
if (!IsAuthorized()) return new Forbidden();
var user = _repo.Find(id);
return user is not null ? user : new NotFound();
}
// Rich results with value-carrying types
Unio<Success<Order>, ValidationError, Conflict> CreateOrder(OrderRequest req)
{
if (!Validate(req, out var errors)) return new ValidationError(errors);
if (HasConflict(req)) return new Conflict();
return new Success<Order>(ProcessOrder(req));
}Unio supports 2 to 20 type parameters (2β9 shown as examples):
| Type | Parameters |
|---|---|
Unio<T0, T1> |
2 types |
Unio<T0, T1, T2> |
3 types |
Unio<T0, T1, T2, T3> |
4 types |
Unio<T0, T1, T2, T3, T4> |
5 types |
Unio<T0, T1, T2, T3, T4, T5> |
6 types |
Unio<T0, T1, T2, T3, T4, T5, T6> |
7 types |
Unio<T0, T1, T2, T3, T4, T5, T6, T7> |
8 types |
Unio<T0, T1, T2, T3, T4, T5, T6, T7, T8> |
9 types |
Unio<T0, ..., T19> |
up to 20 types |
The same arities (2β20) apply to the source generator - UnioBase<T0, ..., T19> supports all 20 arities.
Each Unio<...> (both core and source-generated) provides:
| Member | Return Type | Description |
|---|---|---|
Index |
int |
Zero-based index of the currently stored type |
Value |
object |
Currently stored value (boxed) |
IsT0 .. IsTn |
bool |
Returns true if the union holds the type at that index |
AsT0 .. AsTn |
T0 .. Tn |
Returns the value; throws InvalidOperationException if wrong type |
TryGetT0(out T0) .. TryGetTn(out Tn) |
bool |
Returns true and sets out parameter if the type matches |
Match<TResult>(Func<T0, TResult>, ...) |
TResult |
Exhaustive functional match - one function per type |
Match<TState, TResult>(TState, Func<TState, T0, TResult>, ...) |
TResult |
Allocation-free match - passes state to static lambdas instead of capturing variables |
Switch(Action<T0>, ...) |
void |
Exhaustive side-effect switch - one action per type |
Switch<TState>(TState, Action<TState, T0>, ...) |
void |
Allocation-free switch - passes state to static lambdas instead of capturing variables |
Match<TResult>(Func<T0, Task<TResult>>, ...) |
Task<TResult> |
Exhaustive async functional match |
Match<TState, TResult>(TState, Func<TState, T0, Task<TResult>>, ...) |
Task<TResult> |
Allocation-free async match - passes state to static lambdas |
Switch(Func<T0, Task>, ...) |
Task |
Exhaustive async side-effect switch |
Switch<TState>(TState, Func<TState, T0, Task>, ...) |
Task |
Allocation-free async switch - passes state to static lambdas |
Equals(other) |
bool |
Structural equality via IEquatable<T> |
GetHashCode() |
int |
Hash code based on index + active value |
ToString() |
string |
Delegates to the active value's ToString() |
== / != |
bool |
Value equality operators |
Every union type has implicit conversion operators from each of its type parameters:
Unio<int, string, bool> union;
// All of these work via implicit conversion:
union = 42; // stores int at index 0
union = "hello"; // stores string at index 1
union = true; // stores bool at index 2Unions implement IEquatable<T> with full structural equality:
Unio<int, string> a = 42;
Unio<int, string> b = 42;
Unio<int, string> c = "hello";
a == b; // true - same type, same value
a == c; // false - different types
a != c; // true
// Works in dictionaries and HashSets
var set = new HashSet<Unio<int, string>> { a, b, c };
// set.Count == 2 (a and b are equal)Match and Switch enforce exhaustive handling - every branch must be covered:
// Match returns a value - functional pattern
string result = union.Match(
i => $"integer: {i}",
s => $"string: {s}",
b => $"boolean: {b}");
// Switch executes an action - side-effect pattern
union.Switch(
i => Console.WriteLine($"int: {i}"),
s => Console.WriteLine($"string: {s}"),
b => Console.WriteLine($"bool: {b}"));When a lambda captures a local variable, the compiler creates a new closure object on the heap every time the delegate is invoked. The Match<TState, TResult>, Switch<TState>, overloads eliminate this allocation by passing a state value directly alongside each static lambda:
// β captures `prefix` - allocates a new closure object per call
string result = union.Match(
i => $"{prefix}: {i}",
s => $"{prefix}: {s}",
b => $"{prefix}: {b}");
// β
passes `prefix` as TState to static lambdas - zero allocation
string result = union.Match(prefix,
static (p, i) => $"{p}: {i}",
static (p, s) => $"{p}: {s}",
static (p, b) => $"{p}: {b}");For Switch<TState>, wrap any mutable targets you need to write to in a ValueTuple:
// Passes (logger, config) as a ValueTuple state - no closure allocation
union.Switch((logger, config),
static (s, i) => s.logger.LogInformation("int {V}", i),
static (s, str) => s.logger.LogDebug("string {V}", str),
static (_, b) => { /* ... */ });The same pattern applies to the async variants:
// β captures `db` - allocates per call
string result = await union.Match(
async i => await db.GetIntAsync(i),
async s => await db.GetStringAsync(s));
// β
passes `db` as TState to static lambdas - zero allocation
string result = await union.Match(db,
static async (d, i) => await d.GetIntAsync(i),
static async (d, s) => await d.GetStringAsync(s));
// Switch variant - pass multiple values via ValueTuple
await union.Switch((db, logger),
static async (s, i) => { await s.db.SaveAsync(i); s.logger.LogInformation("Saved int"); },
static async (s, str) => await s.db.LogAsync(str));This pattern is especially valuable in loops, high-throughput pipelines and ASP.NET Core request handlers where per-call allocation matters.
Safe access without exceptions, following the TryParse idiom:
Unio<int, string> result = GetResult();
if (result.TryGetT0(out int number))
{
// number is available here, no exception possible
ProcessNumber(number);
}
else if (result.TryGetT1(out string text))
{
ProcessText(text);
}The Unio.Types package provides 39 high-performance, pre-built types designed for common discriminated union patterns. All marker types are readonly struct with IEquatable<T>, ==/!= operators and [AggressiveInlining] on equality checks.
Marker types are zero-size sentinel structs with no data. They represent states or outcomes:
| Type | Description | Example |
|---|---|---|
Yes |
Affirmative result | Unio<Data, Yes, No> |
No |
Negative result | Unio<Yes, No> |
Maybe |
Indeterminate result | Unio<Yes, No, Maybe> |
True |
Boolean true marker | Unio<True, False> |
False |
Boolean false marker | Unio<True, False> |
Unknown |
Unknown state | Unio<Result, Unknown> |
| Type | Description | Example |
|---|---|---|
All |
All items matched | Unio<All, Some, None> |
Some |
Partial match | Unio<All, Some, None> |
None |
No items / empty result | Unio<Data, None> |
Empty |
Empty / blank | Unio<Content, Empty> |
| Type | Description | Example |
|---|---|---|
Pending |
Operation in progress | Unio<Result, Pending> |
Cancelled |
Operation cancelled | Unio<Result, Cancelled, Timeout> |
Timeout |
Operation timed out | Unio<Result, Timeout> |
Skipped |
Operation was skipped | Unio<Result, Skipped> |
Invalid |
Invalid state / input | Unio<Data, Invalid> |
Disabled |
Feature / resource disabled | Unio<Config, Disabled> |
Expired |
Token / session / resource expired | Unio<Session, Expired> |
RateLimited |
Rate limit hit | Unio<Response, RateLimited> |
| Type | Description | HTTP | Example |
|---|---|---|---|
NotFound |
Resource not found | 404 | Unio<User, NotFound> |
Forbidden |
Access denied | 403 | Unio<User, Forbidden> |
Unauthorized |
Authentication required | 401 | Unio<Data, Unauthorized> |
Conflict |
Resource conflict | 409 | Unio<Updated, Conflict> |
BadRequest |
Invalid request | 400 | Unio<Data, BadRequest> |
Accepted |
Accepted for processing | 202 | Unio<Result, Accepted> |
NoContent |
No content to return | 204 | Unio<Data, NoContent> |
| Type | Description | Example |
|---|---|---|
Created |
Resource was created | Unio<Created, Conflict> |
Updated |
Resource was updated | Unio<Updated, NotFound> |
Deleted |
Resource was deleted | Unio<Deleted, NotFound> |
Unchanged |
No change occurred | Unio<Updated, Unchanged> |
| Type | Description | Example |
|---|---|---|
Success |
Operation succeeded | Unio<Success, Error> |
Error |
Operation failed | Unio<Success, Error> |
Value-carrying types wrap a value of type T with semantic meaning:
| Type | Property | Description | Example |
|---|---|---|---|
Success<T> |
T Value |
Success with result value | new Success<Order>(order) |
Error<T> |
T Value |
Error with details | new Error<string>("msg") |
Result<T> |
T Value |
Generic result wrapper | new Result<int>(42) |
NotFound<T> |
T Value |
Not found with identifier | new NotFound<int>(userId) |
Created<T> |
T Value |
Created with entity/ID | new Created<int>(newId) |
Updated<T> |
T Value |
Updated with entity | new Updated<User>(user) |
ValidationError |
string Message |
Validation error message | new ValidationError("Name required") |
ValidationError<T> |
T Value |
Validation error details | new ValidationError<string[]>(errors) |
All value-carrying types support:
- Implicit conversion from
T-Success<int> s = 42; IEquatable<T>- structural equality on the wrapped value==/!=operatorsToString()- e.g."Success(42)","ValidationError(Name required)"
All 39 types at a glance:
| Category | Types |
|---|---|
| Boolean / Ternary | Yes, No, Maybe, True, False, Unknown |
| Collection | All, Some, None, Empty |
| State | Pending, Cancelled, Timeout, Skipped, Invalid, Disabled, Expired, RateLimited |
| HTTP / API | NotFound, Forbidden, Unauthorized, Conflict, BadRequest, Accepted, NoContent |
| CRUD | Created, Updated, Deleted, Unchanged |
| Result | Success, Error |
| Value Carriers | Success<T>, Error<T>, Result<T>, NotFound<T>, Created<T>, Updated<T>, ValidationError, ValidationError<T> |
The Unio.SourceGenerator is a Roslyn incremental source generator (IIncrementalGenerator). It runs at compile time and generates complete union class implementations from minimal declarations.
Step 1: Declare a partial class with [GenerateUnio] inheriting from UnioBase<...>:
using Unio;
[GenerateUnio]
public partial class StringOrInt : UnioBase<string, int>;Step 2: The generator detects the class at compile time via:
- Syntactic filter - fast check: is it a
partial classwith attributes and a base list? - Semantic filter - does it have
[GenerateUnio]? Does it inherit fromUnioBase<...>? - Code generation - emit a complete
.g.csfile with the full API
Step 3: The generator produces a sealed class that inherits from UnioBase<string, int> - a pre-built abstract base class in the Unio package. All union operations (Match, Switch, TryGet, ValueOr, etc.) are inherited from UnioBase; the generated code only adds the constructor, implicit conversion operators and typed equality members. This keeps generated code minimal while naming types as full classes with class semantics.
For a declaration like
[GenerateUnio]
public partial class Result : UnioBase<User, NotFound, ValidationError>;the generator produces a class inheriting from UnioBase<User, NotFound, ValidationError>. All operations from the base class are immediately available and the generator only emits:
public sealed partial class Result : IEquatable<Result>
{
// Constructor
private Result(Unio<User, NotFound, ValidationError> union) : base(union) { }
// Implicit conversion operators (one per type)
public static implicit operator Result(User value); // β UnioBase.IsT0 etc.
public static implicit operator Result(NotFound value);
public static implicit operator Result(ValidationError value);
// Typed equality (strongly typed to Result - not UnioBase)
public bool Equals(Result? other);
public override bool Equals(object? obj);
public override int GetHashCode();
public static bool operator ==(Result? left, Result? right);
public static bool operator !=(Result? left, Result? right);
}All other members (Index, IsT0βIsT2, AsT0βAsT2, TryGetT0βTryGetT2, Match<TResult>, Match<TState,TResult>, Switch, Switch<TState>, Match<TResult>, Match<TState,TResult>, MapT0βMapT2, ValueOrT0βValueOrT2, ToString, IFormattable, ISpanFormattable, IUtf8SpanFormattable) are inherited from UnioBase.
The source generator reports errors at compile time:
| Code | Severity | Description |
|---|---|---|
UNIO001 |
Error | Class marked with [GenerateUnio] does not inherit from UnioBase<...> |
UNIO002 |
Error | UnioBase<...> has unsupported arity (must be 2β20) |
UNIO003 |
Warning | Duplicate type arguments in UnioBase<...> |
UNIO004 |
Info | Union class should be declared as sealed |
Example:
// UNIO001: Missing UnioBase<...> base class
[GenerateUnio]
public partial class Bad;
// UNIO002: Invalid arity
[GenerateUnio]
public partial class TooFew : UnioBase<int>; // only 1 type - minimum is 2using Unio;
using Unio.Types;
public Unio<User, NotFound, Forbidden> GetUser(int id, ClaimsPrincipal caller)
{
if (!caller.IsInRole("Admin"))
return new Forbidden();
User? user = _repository.Find(id);
if (user is null)
return new NotFound();
return user;
}
// In controller:
var result = GetUser(42, User);
var response = result.Match(
user => Ok(user),
_ => NotFound(),
_ => Forbid());using Unio;
using Unio.Types;
public Unio<Created<int>, ValidationError, Conflict> CreateProduct(ProductDto dto)
{
if (string.IsNullOrEmpty(dto.Name))
return new ValidationError("Name is required");
if (_repo.ExistsByName(dto.Name))
return new Conflict();
int id = _repo.Insert(dto);
return new Created<int>(id);
}
var result = CreateProduct(dto);
result.Switch(
created => Console.WriteLine($"Created with ID: {created.Value}"),
error => Console.WriteLine($"Validation failed: {error.Message}"),
_ => Console.WriteLine("Product already exists"));using Unio;
Unio<int, FormatException, OverflowException> SafeParse(string input)
{
try { return int.Parse(input, CultureInfo.InvariantCulture); }
catch (FormatException ex) { return ex; }
catch (OverflowException ex) { return ex; }
}
var parsed = SafeParse("abc");
var value = parsed.Match(
number => number,
_ => -1, // default for format errors
_ => int.MaxValue); // cap for overflowusing Unio;
using Unio.Types;
public Unio<Success<Order>, ValidationError<string[]>> ValidateAndProcess(OrderRequest req)
{
var errors = new List<string>();
if (req.Quantity <= 0) errors.Add("Quantity must be positive");
if (string.IsNullOrEmpty(req.ProductId)) errors.Add("ProductId is required");
if (req.Price < 0) errors.Add("Price cannot be negative");
if (errors.Count > 0)
return new ValidationError<string[]>(errors.ToArray());
var order = new Order(req.ProductId, req.Quantity, req.Price);
return new Success<Order>(order);
}using Unio;
using Unio.Types;
[GenerateUnio]
public partial class JobState : UnioBase<Pending, Success<JobResult>, Error<string>, Cancelled, Timeout>;
JobState state = new Pending();
// Process...
state = new Success<JobResult>(result);
// Report
Console.WriteLine(state.Match(
_ => "β³ Pending...",
success => $"β
Done: {success.Value}",
error => $"β Failed: {error.Value}",
_ => "π« Cancelled",
_ => "β° Timed out"));using Unio;
using Unio.Types;
// Simple Option<T> via union
Unio<string, None> FindName(int id)
{
var name = _db.FindName(id);
return name is not null ? name : new None();
}
var result = FindName(42);
var display = result.Match(
name => name,
_ => "(unknown)");BenchmarkDotNet v0.14.0, Windows 10 (10.0.19045.6937/22H2/2022Update)
AMD Ryzen 9 9950X, 1 CPU, 32 logical and 16 physical cores
.NET SDK 10.0.103
[Host] : .NET 10.0.3 (10.0.326.7603), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
ShortRun : .NET 10.0.3 (10.0.326.7603), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | Mean | Error | Ratio | Allocated |
|------------------------------------ |----------:|----------:|-------:|----------:|
| | | | | |
| Unio_Match_2Arity | 0.7800 ns | 0.8268 ns | 1.00 | - |
| OneOf_Match_2Arity | 0.7415 ns | 0.2101 ns | 0.95 | - |
| | | | | |
| Unio_Match_5Arity | 1.0782 ns | 1.2554 ns | 1.00 | - |
| OneOf_Match_5Arity | 1.0814 ns | 0.1313 ns | 1.01 | - |
| | | | | |
| Unio_Match_WithState_2Arity | 4.8533 ns | 1.1143 ns | 1.00 | 48 B |
| OneOf_Match_CapturingLambda_2Arity | 5.6056 ns | 0.2197 ns | 1.16 | 72 B |
| | | | | |
| Unio_Match_WithState_5Arity | 5.0281 ns | 0.6369 ns | 1.00 | 48 B |
| OneOf_Match_CapturingLambda_5Arity | 5.7137 ns | 0.2740 ns | 1.14 | 72 B |
| | | | | |
| Unio_Switch_5Arity | 0.0000 ns | 0.0000 ns | ? | - |
| OneOf_Switch_5Arity | 0.0196 ns | 0.0467 ns | ? | - |
| | | | | |
| Unio_Switch_WithState_2Arity | 0.6918 ns | 0.0947 ns | 1.00 | - |
| OneOf_Switch_CapturingLambda_2Arity | 1.9457 ns | 0.5249 ns | 2.81 | 32 B |
| | | | | |
| Unio_Switch_WithState_5Arity | 1.1188 ns | 0.1413 ns | 1.00 | - |
| OneOf_Switch_CapturingLambda_5Arity | 1.9274 ns | 0.0991 ns | 1.72 | 32 B |
| | | | | |
| Unio_ToString | 0.4430 ns | 0.0746 ns | 1.00 | - |
| OneOf_ToString | 5.1127 ns | 0.3844 ns | 11.54 | 56 B |
| | | | | |
| Unio_TryGetT0_5Arity | 0.0014 ns | 0.0432 ns | ? | - |
| OneOf_TryPickT0_5Arity | 2.6490 ns | 0.1678 ns | ? | - |
| | | | | |
| Unio_TryGetT4_5Arity_Miss | 0.0149 ns | 0.0103 ns | 1.00 | - |
| OneOf_TryPickT4_5Arity_Miss | 4.1028 ns | 0.6939 ns | 276.00 | - |
| | | | | |
| Unio_TryGetT0_Hit | 0.0000 ns | 0.0000 ns | ? | - |
| OneOf_TryPickT0_Hit | 0.2008 ns | 0.1185 ns | ? | - |
| | | | | |
| Unio_TryGetT1_Miss | 0.0185 ns | 0.0697 ns | 1.03 | - |
| OneOf_TryPickT1_Miss | 0.1901 ns | 0.0629 ns | 10.52 | - |Unio is built for maximum runtime performance:
| Decision | Benefit |
|---|---|
readonly struct for core type |
No heap allocation - stack-allocated for small value types |
UnioBase<...> abstract class |
Named (source-generated) union types get class semantics and reference identity |
Typed generic fields (T0? _value0) |
No object boxing - value types stored directly |
[MethodImpl(AggressiveInlining)] |
JIT inlines all property accessors, TryGet, Match, Switch and operators |
byte _index discriminator |
Minimal overhead: 1 byte to track the active type |
switch expressions |
JIT compiles to efficient jump tables |
| TieredPGO / DynamicPGO enabled | Profile-guided optimization for hot paths |
| Source-generated named types | Inherit from UnioBase - only constructor + implicit operators generated |
| Marker types (empty structs) | 1 byte size, zero-cost equality, AggressiveInlining |
Match<TState, TResult> / Switch<TState> |
State passed as parameter to static lambdas - capturing closures never allocated |
Run benchmarks yourself:
dotnet run --configuration Release --project perf/Unio.Benchmarks/Unio.Benchmarks.csprojExpected characteristics:
- Creation: Simple heap allocation via private constructor
- IsT# / Index: Single byte comparison, fully inlined
- AsT#: Single byte comparison + field access, fully inlined
- TryGet: Single byte comparison + field access + bool return, fully inlined
- Match / Switch:
switchexpression compiled to jump table by JIT - Match<TState> / Switch<TState>: Same as above, but with delegate arguments that are
staticβ 0 B allocated vs. a closure object per call for the capturing variant - Equality: Index comparison +
EqualityComparer<T>.Default, fully inlined
Unio was inspired by OneOf, which pioneered discriminated unions in C#. However, a more modern, high-performance implementation was needed - with a readonly struct core type, UnioBase<...> for named class-based unions, typed generic fields and full value equality semantics.
| Feature | OneOf | Unio |
|---|---|---|
| Core type | struct (OneOf) / class (OneOfBase) |
readonly struct |
| Named union base class | OneOfBase<...> abstract class |
UnioBase<...> abstract class |
| Value Storage | object field (boxing for value types) |
Typed generic fields |
| Source Generator | Basic: constructor + implicit operators | Inherits from UnioBase - only constructor + implicit operators generated |
| Pre-built Types | 13 types (5 in Assorted.cs + 4 named unions) | 39 types across 7 categories |
TryGet Pattern |
β Not available | β
TryGetT0(out T0 value) .. TryGetTn(out Tn value) |
| Allocation-free Match / Switch | β Capturing lambdas only | β
Match<TState, TResult> / Switch<TState> with static lambdas |
IEquatable<T> |
β Not implemented | β Full structural equality |
== / != Operators |
β Not available | β Value equality operators |
AggressiveInlining |
β Not marked | β On all property accessors and methods |
| Max Arity | Up to 9 (OneOf.Extended) | 2β20 built-in |
| Value-Carrying Types | Success<T>, Error<T>, Result<T> |
All of OneOf's + NotFound<T>, Created<T>, Updated<T>, ValidationError, ValidationError<T> |
| Marker Type Implementation | Mixed: classes (nested) + structs | All readonly struct with IEquatable<T> |
| Target Frameworks | netstandard2.0 | net8.0, net9.0, net10.0 |
