OneOf\ API Reference
OneOf<T1, T2, T3> represents a value that can be one of three different types. It's ideal for complex state machines, API responses with multiple error types, or scenarios requiring more than two distinct states.
Creates a OneOf<T1, T2, T3> containing a T1 value.
OneOf<Success, ClientError, ServerError> result = OneOf<Success, ClientError, ServerError>.FromT1(new Success("Operation completed"));
Parameters:
- value (T1): The T1 value to wrap
Returns: OneOf<T1, T2, T3> containing the T1 value
Creates a OneOf<T1, T2, T3> containing a T2 value.
OneOf<Success, ClientError, ServerError> result = OneOf<Success, ClientError, ServerError>.FromT2(new ClientError("Invalid input", 400));
Parameters:
- value (T2): The T2 value to wrap
Returns: OneOf<T1, T2, T3> containing the T2 value
Creates a OneOf<T1, T2, T3> containing a T3 value.
OneOf<Success, ClientError, ServerError> result = OneOf<Success, ClientError, ServerError>.FromT3(new ServerError("Database timeout", 500));
Parameters:
- value (T3): The T3 value to wrap
Returns: OneOf<T1, T2, T3> containing the T3 value
Three-way pattern matching that executes the appropriate function based on the contained type.
string message = apiResult.Match(
case1: success => $"Success: {success.Message}",
case2: clientError => $"Client Error: {clientError.Message}",
case3: serverError => $"Server Error: {serverError.Message}"
);
Parameters:
- case1 (Funccase2 (Funccase3 (Func
Returns: Result of executing the appropriate function
Throws: ArgumentNullException if any parameter is null
Executes side effects based on the contained type.
apiResult.Switch(
case1: success => LogSuccess(success),
case2: clientError => LogClientError(clientError),
case3: serverError => LogServerError(serverError)
);
Parameters:
- case1 (Actioncase2 (Actioncase3 (Action
Throws: ArgumentNullException if any parameter is null
Transforms the T2 value while preserving T1 and T3 values unchanged.
OneOf<Success, ClientError, ServerError> result = CallApi();
OneOf<Success, FriendlyError, ServerError> mapped = result.MapT2(error => error.ToFriendlyError());
Parameters:
- mapper (Func
Returns: OneOf<T1, TResult, T3> with transformed T2 or original T1/T3
Throws: ArgumentNullException if mapper is null
Transforms the T3 value while preserving T1 and T2 values unchanged.
OneOf<Success, ClientError, ServerError> result = CallApi();
OneOf<Success, ClientError, RetryableError> mapped = result.MapT3(error => error.ToRetryableError());
Parameters:
- mapper (Func
Returns: OneOf<T1, T2, TResult> with transformed T3 or original T1/T2
Throws: ArgumentNullException if mapper is null
Chains OneOf operations, flattening nested OneOf types for T2.
OneOf<Success, ClientError, ServerError> result = CallApi();
OneOf<Success, ValidatedClientError, ServerError> validated = result.BindT2(error => ValidateClientError(error));
Parameters:
- binder (Func
Returns: OneOf<T1, TResult, T3> from binder or original T1/T3
Throws: ArgumentNullException if binder is null
BindT3(Func> binder)
Chains OneOf operations, flattening nested OneOf types for T3.
OneOf<Success, ClientError, ServerError> result = CallApi();
OneOf<Success, ClientError, ProcessedServerError> processed = result.BindT3(error => ProcessServerError(error));
Parameters:
- binder (Func
Returns: OneOf<T1, T2, TResult> from binder or original T1/T2
Throws: ArgumentNullException if binder is null
IsT1
Gets whether the OneOf contains a T1 value.
if (result.IsT1)
{
var success = result.AsT1;
Console.WriteLine($"Success: {success.Message}");
}
Returns: true if containing T1, false otherwise
IsT2
Gets whether the OneOf contains a T2 value.
if (result.IsT2)
{
var clientError = result.AsT2;
Console.WriteLine($"Client Error: {clientError.Message}");
}
Returns: true if containing T2, false otherwise
IsT3
Gets whether the OneOf contains a T3 value.
if (result.IsT3)
{
var serverError = result.AsT3;
Console.WriteLine($"Server Error: {serverError.Message}");
}
Returns: true if containing T3, false otherwise
AsT1
Gets the value as T1. Throws if containing T2 or T3.
if (result.IsT1)
{
Success success = result.AsT1; // Safe to access
}
Returns: The contained T1 value
Throws: InvalidOperationException if containing T2 or T3
AsT2
Gets the value as T2. Throws if containing T1 or T3.
if (result.IsT2)
{
ClientError clientError = result.AsT2; // Safe to access
}
Returns: The contained T2 value
Throws: InvalidOperationException if containing T1 or T3
AsT3
Gets the value as T3. Throws if containing T1 or T2.
if (result.IsT3)
{
ServerError serverError = result.AsT3; // Safe to access
}
Returns: The contained T3 value
Throws: InvalidOperationException if containing T1 or T2
ToTwoWay(Func mapT3ToT1, Func mapT3ToT2)
Converts to a 2-way OneOf by mapping T3 to either T1 or T2.
OneOf<Success, ClientError, ServerError> threeWay = CallApi();
OneOf<Success, ClientError> twoWay = threeWay.ToTwoWay(
mapT3ToT1: serverError => new Success($"Retry: {serverError.Message}"),
mapT3ToT2: serverError => new ClientError(serverError.Message, 503)
);
Parameters:
- mapT3ToT1 (FuncmapT3ToT2 (Func
Returns: OneOf<T1, T2> with mapped T3 or original T1/T2
Throws: ArgumentNullException if any parameter is null
ToTwoWayWithFallback(T1 fallbackT1, T2 fallbackT2)
Converts to a 2-way OneOf by providing fallback values for T3.
OneOf<Success, ClientError, ServerError> threeWay = CallApi();
OneOf<Success, ClientError> twoWay = threeWay.ToTwoWayWithFallback(
fallbackT1: new Success("Default success"),
fallbackT2: new ClientError("Default error", 500)
);
Parameters:
- fallbackT1 (T1): Fallback T1 value when original is T3
- fallbackT2 (T2): Fallback T2 value when original is T3
Returns: OneOf<T1, T2> with fallback values or original T1/T2
ToThreeWay(this OneOf oneOf, T3 fallbackT3)
Converts a 2-way OneOf to 3-way by adding a T3 fallback.
OneOf<Error, User> twoWay = GetUser(id);
OneOf<Error, User, SystemError> threeWay = twoWay.ToThreeWay(new SystemError("System unavailable"));
Parameters:
- oneOf (OneOffallbackT3 (T3): The T3 value to use when converting
Returns: OneOf<T1, T2, T3> with original value or T3 fallback
Equals(object? obj)
Compares OneOf values for equality.
OneOf<Success, ClientError, ServerError> a = OneOf<Success, ClientError, ServerError>.FromT1(new Success("OK"));
OneOf<Success, ClientError, ServerError> b = OneOf<Success, ClientError, ServerError>.FromT1(new Success("OK"));
bool equal = a.Equals(b); // true
Parameters:
- obj (object?): Object to compare with
Returns: true if equal, false otherwise
Equals(OneOf other)
Compares with another OneOf of the same types.
OneOf<Success, ClientError, ServerError> a = GetApiResult();
OneOf<Success, ClientError, ServerError> b = GetApiResult();
bool equal = a.Equals(b); // true if same result
Parameters:
- other (OneOf
Returns: true if equal, false otherwise
GetHashCode()
Gets hash code for the OneOf value.
int hash = result.GetHashCode();
Returns: Hash code based on contained value and type
ToString()
String representation of the OneOf.
OneOf<Success, ClientError, ServerError> result = OneOf<Success, ClientError, ServerError>.FromT1(new Success("OK"));
Console.WriteLine(result.ToString()); // "OneOf<Success, ClientError, ServerError>(T1: Success { Message = OK })"
Returns: String representation showing type and value
OneOf
OneOf<Error, int, string> a = OneOf<Error, int, string>.FromT2(2);
OneOf<Error, int, string> b = OneOf<Error, int, string>.FromT2(3);
OneOf<Error, int, string> result = from x in a
from y in b
select x + y; // T2: 5
API Response with Three States
public OneOf<Success, ClientError, ServerError> CallApi(string endpoint)
{
try
{
var response = _httpClient.GetAsync(endpoint).Result;
return response.StatusCode switch
{
HttpStatusCode.OK => OneOf<Success, ClientError, ServerError>.FromT1(new Success("Request successful")),
HttpStatusCode.BadRequest => OneOf<Success, ClientError, ServerError>.FromT2(new ClientError("Bad request", 400)),
HttpStatusCode.NotFound => OneOf<Success, ClientError, ServerError>.FromT2(new ClientError("Not found", 404)),
_ => OneOf<Success, ClientError, ServerError>.FromT3(new ServerError("Server error", (int)response.StatusCode))
};
}
catch (HttpRequestException ex)
{
return OneOf<Success, ClientError, ServerError>.FromT3(new ServerError(ex.Message, 500));
}
}
public string ProcessApiCall(string endpoint)
{
return CallApi(endpoint).Match(
case1: success => $"✅ {success.Message}",
case2: clientError => $"⚠️ Client error: {clientError.Message}",
case3: serverError => $"❌ Server error: {serverError.Message}"
);
}
Configuration Parsing with Multiple Types
public OneOf<ParseError, int, bool> ParseConfigValue(string key, string value)
{
return key.ToLowerInvariant() switch
{
"timeout" when int.TryParse(value, out var timeout) && timeout > 0 =>
OneOf<ParseError, int, bool>.FromT2(timeout),
"enabled" when bool.TryParse(value, out var enabled) =>
OneOf<ParseError, int, bool>.FromT3(enabled),
"timeout" => OneOf<ParseError, int, bool>.FromT1(new ParseError("Invalid timeout value")),
"enabled" => OneOf<ParseError, int, bool>.FromT1(new ParseError("Invalid enabled value")),
_ => OneOf<ParseError, int, bool>.FromT1(new ParseError($"Unknown config key: {key}"))
};
}
public void ApplyConfiguration()
{
var timeout = ParseConfigValue("timeout", _config["timeout"]);
var enabled = ParseConfigValue("enabled", _config["enabled"]);
timeout.Match(
case1: error => Console.WriteLine($"Timeout error: {error.Message}"),
case2: value => _settings.Timeout = value,
case3: _ => Console.WriteLine("Timeout config is boolean, expected integer")
);
enabled.Match(
case1: error => Console.WriteLine($"Enabled error: {error.Message}"),
case2: _ => Console.WriteLine("Enabled config is integer, expected boolean"),
case3: value => _settings.Enabled = value
);
}
Database Operation States
public OneOf<Created, Updated, NotFound> SaveUser(User user)
{
var existing = _database.FindUser(user.Id);
if (existing == null)
{
_database.Insert(user);
return OneOf<Created, Updated, NotFound>.FromT1(new Created(user.Id));
}
else if (existing.Version < user.Version)
{
_database.Update(user);
return OneOf<Created, Updated, NotFound>.FromT2(new Updated(user.Id));
}
else
{
return OneOf<Created, Updated, NotFound>.FromT3(new NotFound(user.Id));
}
}
public string HandleUserSave(User user)
{
return SaveUser(user).Match(
case1: created => $"✅ User created with ID: {created.Id}",
case2: updated => $"📝 User updated with ID: {updated.Id}",
case3: notFound => $"❌ User not found or version conflict: {notFound.Id}"
);
}
State Machine Implementation
public OneOf<Idle, Processing, Completed> ProcessWorkflow(WorkflowState state)
{
return state.Status switch
{
WorkflowStatus.Pending => OneOf<Idle, Processing, Completed>.FromT2(new Processing(state.Id)),
WorkflowStatus.Processing => state.IsComplete
? OneOf<Idle, Processing, Completed>.FromT3(new Completed(state.Id))
: OneOf<Idle, Processing, Completed>.FromT2(new Processing(state.Id)),
WorkflowStatus.Completed => OneOf<Idle, Processing, Completed>.FromT3(new Completed(state.Id)),
_ => OneOf<Idle, Processing, Completed>.FromT1(new Idle())
};
}
public void UpdateWorkflow(WorkflowState state)
{
ProcessWorkflow(state).Switch(
case1: idle => Console.WriteLine("Workflow is idle"),
case2: processing => Console.WriteLine($"Workflow {processing.Id} is processing"),
case3: completed => Console.WriteLine($"Workflow {completed.Id} is completed")
);
}
Conversion Between 2-way and 3-way
public OneOf<Error, User> GetUserSimple(int id)
{
var result = GetUserDetailed(id);
// Convert 3-way to 2-way by mapping server errors to regular errors
return result.ToTwoWay(
mapT3ToT1: serverError => new Error($"Server error: {serverError.Message}"),
mapT3ToT2: serverError => new User("Fallback")
);
}
public OneOf<Success, ClientError, ServerError> GetUserDetailed(int id)
{
var simpleResult = GetUserSimple(id);
// Convert 2-way to 3-way by adding server error fallback
return simpleResult.ToThreeWay(new ServerError("Service unavailable", 503));
}
- Memory Allocation: Single allocation for the entire OneOf instance
- Type Safety: Compile-time guarantees about contained types
- Pattern Matching: Optimized for common T1/T2/T3 patterns
- Discriminator: Small overhead for tracking which type is contained
✅ Do Use
- Descriptive type names that clearly indicate the relationship
- Specific error types for different error categories
- Pattern matching for handling all cases explicitly
- Conversions between 2-way and 3-way when needed
❌ Avoid
- Using OneOf
when OneOf would suffice - Magic strings or objects as generic types
- Nested OneOf without proper flattening
- Forgetting to handle all cases in pattern matching
| Situation | Use OneOf |
Use OneOf |
|---|---|---|
| Binary success/error | ✅ | ❌ |
| Multiple error types | ❌ | ✅ |
| Three distinct states | ❌ | ✅ |
| Simple API responses | ✅ | ❌ |
| Complex state machines | ❌ | ✅ |
- Advanced Patterns Guide - Usage patterns and best practices
- OneOf API - 2-way discriminated unions
- Integration Extensions - Result integration methods