UnionResult 1.0.0-preview.1

This is a prerelease version of UnionResult.
dotnet add package UnionResult --version 1.0.0-preview.1
                    
NuGet\Install-Package UnionResult -Version 1.0.0-preview.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="UnionResult" Version="1.0.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="UnionResult" Version="1.0.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="UnionResult" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add UnionResult --version 1.0.0-preview.1
                    
#r "nuget: UnionResult, 1.0.0-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package UnionResult@1.0.0-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=UnionResult&version=1.0.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=UnionResult&version=1.0.0-preview.1&prerelease
                    
Install as a Cake Tool

<p align="center"> <img src="https://raw.githubusercontent.com/Adolfok3/UnionResult/main/assets/icon.png" alt="UnionResult" width="150" /> </p>

UnionResult

NuGet License: MIT .NET 11 C# union types main tests tests count codecov

A Result pattern implementation for .NET, built on top of C#'s new native union types.

Why

UnionResult isn't about functional programming for its own sake - it exists to avoid unnecessary exception overhead in everyday application code. Throwing and catching exceptions is expensive and meant for truly exceptional situations, not for outcomes an operation can expect and a caller should routinely handle (validation failures, "not found", a downstream call that failed). Result/Result<T> let a method return both possible outcomes explicitly, so the caller decides how to react instead of wrapping every call in try/catch.

Several packages already implement this pattern for .NET. What sets UnionResult apart is the implementation: instead of a hand-rolled discriminated-union struct, it's built directly on top of C#'s new native union type (introduced as a preview language feature in .NET 11) - success/failure is a real, compiler-recognized union with exhaustive pattern-matching support, not a struct that merely looks like one.

Requirements

Union types are a preview C# language feature. To use this package your project needs:

<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>

and the .NET 11 preview SDK.

Installation

dotnet add package UnionResult

Or via the Package Manager Console:

Install-Package UnionResult

Usage

Result<T> - an operation that returns a value

using UnionResult;

Result<int> Divide(int a, int b) =>
    b == 0
        ? Result<int>.Failure(new DivideByZeroException())
        : Result<int>.Success(a / b);

var result = Divide(10, 2);

var message = result.IsSuccess
    ? $"Result: {result.AsValue()}"
    : $"Failed: {result.AsException().Message}";

Result - an operation with no return value

using UnionResult;

Result Save(User user) =>
    repository.TrySave(user, out var error)
        ? Result.Success()
        : Result.Failure(error);

var result = Save(user);

if (result.IsFailure)
{
    logger.LogError(result.AsException(), "Failed to save user");
}

Implicit conversions

Success/Failure aren't the only way to build a result - since Result<T> and Result are real C# unions, a value of a case type converts implicitly, so you can just return it directly:

using UnionResult;

Result<int> Divide(int a, int b)
{
    if (b == 0)
    {
        return new DivideByZeroException(); // implicitly converts to Result<int>
    }

    return a / b; // implicitly converts to Result<int>
}

Result<int> r = 42; // same conversion, assigned directly

The same applies to the failure case of the non-generic Result (there's no implicit conversion for "success with no value", so that side still calls Result.Success() explicitly):

Result Save(User user)
{
    if (!repository.TrySave(user, out var error))
    {
        return error; // implicitly converts to Result
    }

    return Result.Success();
}

A more realistic example: replacing exceptions at a service boundary

public interface IUserRepository
{
    Result<User> GetById(int id);
}

public class UserService(IUserRepository repository)
{
    public string GetDisplayName(int id)
    {
        var result = repository.GetById(id);
        return result.IsSuccess ? result.AsValue().Name : "Unknown user";
    }
}

Here GetById communicates "this can fail" through its return type. The caller isn't forced to guess whether the repository throws, and no try/catch is needed just to keep the app running when a lookup fails.

Pattern matching

Because Result<T> is a real C# union, it also supports exhaustive pattern matching:

var message = result switch
{
    int value => $"Result: {value}",
    Exception ex => $"Failed: {ex.Message}",
};

API reference

Member Description
Result<T>.Success(T value) / Result.Success() Creates a successful result.
Result<T>.Failure(Exception) / Result.Failure(Exception) Creates a failed result. Throws ArgumentNullException if exception is null.
IsSuccess / IsFailure Whether the result is a success or a failure.
AsValue() (Result<T> only) Returns the success value, or throws InvalidOperationException if the result is a failure.
AsException() Returns the failure's exception, or throws InvalidOperationException if the result is a success.
TryGetValue(out T value) / TryGetValue(out Exception value) Non-throwing variants of AsValue()/AsException().
HasValue Whether the result holds any case at all (false only for default(Result<T>)).

Benchmarks

UnionResult was benchmarked against Divino.OperationResult, a similar Result-pattern package, across three scenarios: a value-type payload (int), a reference-type payload (Product), and no payload at all. Each scenario measures creating a result (Create) and reading an already-built one (Read), for both the success and failure paths.

Environment: BenchmarkDotNet v0.15.8, Windows 11, AMD Ryzen 7 7800X3D 4.20GHz, .NET SDK 11.0.100-preview.5, in-process toolchain.

Category Operation UnionResult OperationResult Allocated Winner
Value type (int) Create Success 0.0044 ns 0.0000 ns 0 B (both) tie (noise)
Create Failure 0.2049 ns 0.6265 ns 0 B (both) UnionResult (~3x)
Read Success 0.4157 ns 0.0023 ns 0 B (both) OperationResult
Read Failure 0.6244 ns 0.2201 ns 0 B (both) OperationResult (~3x)
Reference type (Product) Create Success 0.1977 ns 0.2250 ns 0 B (both) tie
Create Failure 0.2374 ns 1.1618 ns 0 B (both) UnionResult (~5x)
Read Success 0.6298 ns 0.2062 ns 0 B (both) OperationResult (~3x)
Read Failure 0.6286 ns 0.2114 ns 0 B (both) OperationResult (~3x)
No payload Create Success 0.0001 ns 0.0024 ns 0 B (both) tie (noise)
Create Failure 0.2248 ns 0.6071 ns 0 B (both) UnionResult (~2.7x)
Read Success 0.0327 ns 0.0005 ns 0 B (both) tie (noise)
Read Failure 0.6227 ns 0.2338 ns 0 B (both) OperationResult (~2.7x)

Takeaways:

  • Neither package allocates in any scenario measured.
  • UnionResult matches or beats OperationResult on every Create scenario - notably ~3-5x faster creating a failure.
  • OperationResult is consistently faster to read an already-built result (~0.2-0.4 ns) - AsValue()/AsException() validate the result's state before returning, which is the cost of that safety check.
  • Values under ~0.03 ns aren't meaningful differences - that's the measurement floor of the benchmark itself.

The benchmark source lives in benchmarks/UnionResult.Benchmarks.

License

MIT - see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net11.0 is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-preview.1 34 7/19/2026