C# is coming ‘soon’ with Type Unions (see the official proposal and Nick’s video on this), which I think is great. However it’s not there yet and if you need it now it’s important t consider which library will require the least refactoring when this feature becomes part of the official language specification.
This post explores some of the most popular libraries in .NET for implementing this pattern and helps you decide which one fits your project’s needs.
You can find all the code snippets in this repository: woutervanranst/ResultLibraries
Target Syntax
As an example use case, imagine a method that returns either a Success or an Error (a typical use case in MediatR) – and we don’t want to rely on throwing an Exception for flow control.
This would be the (hypothetical) C# code once type unions are implemented in the language
public record Success(string Message);
public record Error(string Message);
// Future C# (hypothetical)
union Result { Success; Error; }
Result result = ...
string message = result switch { Success s => s.Message, Error e => e.Message };
1. FluentResults (GitHub)
Refactoring Effort: ⭐⭐⭐ Medium
FluentResults is not really a type union, rather a library for the Result pattern, ideal for CQRS workflows. Depending on your use case, this may be ‘good enough’. However, it will require a significant refactor down the line.
Syntax
using FluentResults;
Result<string> result = Result.Ok("Hello World"); // Has a built-in Result type
Result<string> failureResult = Result.Fail("Something went wrong");
var message = result.IsSuccess
? $"Success: {result.Value}"
: $"Error: {string.Join(", ", result.Errors)}";
Console.WriteLine(message);
Pros
- Specialized for Result Handling: Offers
Result<T>
,Result
, andResultBase
with rich error metadata and supports error chaining, nested errors, and success/error message aggregation. - Minimal boilerplate (built-in Result type) and easy adoption in existing codebases.
Cons
- Not really a type union, so will require an more extensive refactor.
- Not for other type unions (eg. BillingAmount = kWh | m3)
2. CSharpFunctionalExtensions (GitHub)
Refactoring Effort: ⭐⭐⭐ Medium
Overview
Like FluentResults (not really a type union) also provides a Maybe<T>
construct (~~ explicit nullability for reference types) and some other functional-programming-inspired constructs.
Syntax
using CSharpFunctionalExtensions;
Result<string> result = Result.Success("Hello World"); // Has a built-in Result type
Result<string> failureResult = Result.Failure<string>("Something went wrong");
string message = result.IsSuccess
? $"Success: {result.Value}"
: $"Error: {result.Error}";
Console.WriteLine(message);
Pros
- Simple API for basic success/failure.
Cons
- Not really a type union, so will require an more extensive refactor.
- Not for other type unions (eg. BillingAmount = kWh | m3)
3. OneOf (GitHub)
Refactoring Effort: ⭐ Low
Truly models discriminated unions using generics (OneOf<T1, T2>
), aligning directly with C#’s proposed native union syntax.
Syntax
using OneOf;
OneOf<Success, Error> result = OneOf<Success, Error>.FromT0(new Success("Hello World"));
OneOf<Success, Error> failureResult = OneOf<Success, Error>.FromT1(new Error("Something went wrong"));
string message = result.Match(
success => $"Success: {success.Message}",
error => $"Error: {error.Message}"
);
Console.WriteLine(message);
public record Success(string Message);
public record Error(string Message);
Pros
- Minimal code changes required.
- Enforces exhaustive case handling via
Switch()
/Match()
.
4. LanguageExt (GitHub)
Refactoring Effort: ⭐ Low
Like OneOf (truly models discriminated unions) but also provides more functional-programming-inspired constructs (Option
, Try
, …).
Syntax
using LanguageExt;
Either<Success, Error> result = new Success("Hello World");
Either<Success, Error> failureResult = new Error("Something went wrong");
string message = result.Match(
Left: msg => $"Success: {msg}",
Right: err => $"Error: {err}"
);
Console.WriteLine(message);
public record Success(string Message);
public record Error(string Message);
Pros
- Minimal code changes required.
- Enforces exhaustive case handling via
Switch()
/Match()
. - Upramp to other functional programming concepts such as
Option
andTry
.
Cons
- Likely overkill if you only want discriminated unions.
- Steep learning curve if you’re not familiar with functional programming.
5. Custom Result Types
Refactoring Effort: ⭐⭐⭐⭐ High
Overview
You may opt to roll your own Result<T>
record. This gives you maximum flexibility, but you are reinventing the wheel here and risk a significant refactor down the line.
Syntax
public record Result<T>(bool IsSuccess, T? Data, string? Error);
public class Command : IRequest<Result<string>> { }
public class Handler : IRequestHandler<Command, Result<string>>
{
public Task<Result<string>> Handle(Command command, CancellationToken token)
=> Task.FromResult(new Result<string>(true, "Done"));
}
Pros
- Highly customizable.
Cons
- No built-in union features or pattern matching.
Community Adoption
Looking at community adoption, OneOf is the winner.

Summary
Library | Learning Curve | Refactoring Effort | Key Strengths | Community Adoption |
---|---|---|---|---|
FluentResults | ⭐⭐⭐⭐ Lowest | ⭐⭐⭐ | Simple Result<T> Library | #4 |
CSharpFunctionalExtensions | ⭐⭐⭐ | ⭐⭐⭐ | Simple Result<T> Library | #3 |
OneOf | ⭐⭐ | ⭐ Lowest | Future proof Result<T> | #1 |
LanguageExt | ⭐ Highest | ⭐ Lowest | Best for functional programming | #2 |
Custom Result Type | ⭐⭐⭐⭐ Highest | YMMV 😉 | N/A |
If you have a crystal ball and can predict your future use cases, this is your decision tree:
Not aligned with future C# type union | Aligned with future C# type union | |
Use Case = Result Pattern only | FluentResult | OneOf |
Use Case = Functional Programming | CSharpFunctionExtension | LanguageExt |
FluentResult is likely the solution for your immediate need, and LanguageExt is probably overkill if you are reading this (and I assume you are new to FP). In combination with the #1 spot on the community adoption, OneOf probably strikes the best balance.