Svan.Monads 2.0.3

dotnet add package Svan.Monads --version 2.0.3
                    
NuGet\Install-Package Svan.Monads -Version 2.0.3
                    
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="Svan.Monads" Version="2.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Svan.Monads" Version="2.0.3" />
                    
Directory.Packages.props
<PackageReference Include="Svan.Monads" />
                    
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 Svan.Monads --version 2.0.3
                    
#r "nuget: Svan.Monads, 2.0.3"
                    
#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 Svan.Monads@2.0.3
                    
#: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=Svan.Monads&version=2.0.3
                    
Install as a Cake Addin
#tool nuget:?package=Svan.Monads&version=2.0.3
                    
Install as a Cake Tool

Build, Test & Publish Nuget

Svan.Monads

This library adds some common monads to C#, enabling "Railway Oriented Programming" and functional programming styles. It includes the Option<T>, Result<TError, TSuccess>, Try<TSuccess>, and Either<TLeft, TRight> monads, along with extension methods for fluent chaining, async, and error handling.

Installation

dotnet add package Svan.Monads

Code Examples

Breaking changes in version 2.x

The library no longer depends on OneOf. The monads now inherit from a built-in Union<TLeft, TRight> base class that is discriminated using Left<T> and Right<T> wrapper types.

OneOfBase is no longer the base class

The Match and Switch methods from OneOf (which received wrapper types like Error<T>, Success<T>, Some<T>) are replaced by Fold, which receives the unwrapped values directly.

  // Option: Match/Switch took None and Some<T>
- option.Match(
-     none => "nothing",
-     some => $"got {some.Value}");
+ option.Fold(
+     () => "nothing",
+     value => $"got {value}");

  // Option: void side-effects used Switch
- option.Switch(
-     none => Console.WriteLine("nothing"),
-     some => Console.WriteLine(some.Value));
+ option.Do(value => Console.WriteLine(value))
+       .DoIfNone(() => Console.WriteLine("nothing"));

  // Result: Match/Switch took Error<TError> and Success<TSuccess>
- result.Match(
-     error => $"failed: {error.Value}",
-     success => $"got {success.Value}");
+ result.Fold(
+     error => $"failed: {error}",
+     success => $"got {success}");

  // Result: void side-effects used Switch
- result.Switch(
-     error => Console.WriteLine(error.Value),
-     success => Console.WriteLine(success.Value));
+ result.Do(success => Console.WriteLine(success))
+       .DoIfError(error => Console.WriteLine(error));

Error<T>, Success<T> and Some<T> have been removed

These OneOf wrapper types are no longer needed. Union now uses Left<T> and Right<T> internally for discrimination, and all public APIs work with the unwrapped values directly.

- Option<int> option = new Some<int>(42);
+ Option<int> option = Option<int>.Some(42);

- Result<string, int> result = new Success<int>(42);
- Result<string, int> error = new Error<string>("fail");
+ var result = Result<string, int>.Success(42);
+ var error = Result<string, int>.Error("fail");

Result and Try no longer have implicit conversions

Construction now uses explicit factory methods instead of implicit operators.

- Result<string, int> result = 42;
- Result<string, int> error = "something went wrong";
+ var result = Result<string, int>.Success(42);
+ var error = Result<string, int>.Error("something went wrong");

- Try<int> tried = 42;
- Try<int> failed = new Exception("boom");
+ var tried = Try<int>.Success(42);
+ var failed = Try<int>.Exception(new Exception("boom"));

The Option Monad

The Option<T> monad represents a value that may or may not exist. It is modeled after F#'s Option Type and is functionally similar to Haskell's Maybe Monad.

Constructing

// Using factory methods
var some = Option<int>.Some(42);
var none = Option<int>.None();

// Using the non-generic static class
var some2 = Option.Some(42);
var none2 = Option.None<int>();

// Using the extension method (null becomes None for reference types)
var fromValue = 42.ToOption();

Monadic operations

var option = Option.Some(42);

var result = option
    .Filter(i => i > 10)
    .Map(i => i * 2)
    .Bind(i => i % 2 == 0
        ? Option<string>.Some($"even: {i}")
        : Option<string>.None())
    .Do(Console.WriteLine);

Unboxing

var option = Option.Some(42);

// Fold with handlers for both cases
string message = option.Fold(
    () => "nothing here",
    value => $"got {value}");

// Get the value or a default
int value = option.DefaultWith(() => -1);

// Get the value or throw
int certain = option.OrThrow();

// Convert to Result
Result<string, int> result = option.ToResult(() => "was none");

The Result Monad

The Result<TError, TSuccess> monad represents an operation that can either succeed with a TSuccess value or fail with a TError value. It enables railway-oriented error handling and readable data transformation pipelines.

Constructing

// Using factory methods
var success = Result<string, int>.Success(42);
var error = Result<string, int>.Error("something went wrong");

// Using the non-generic static class
var success2 = Result.Success<string, int>(42);
var error2 = Result.Error<string, int>("something went wrong");

// Using extension methods
var success3 = 42.ToSuccess<string, int>();
var error3 = "something went wrong".ToError<string, int>();

Monadic operations

Result<string, int> Divide(int number, int by)
    => by == 0
        ? Result<string, int>.Error("division by zero")
        : Result<string, int>.Success(number / by);

var result = Divide(12, 2)
    .Bind(n => Divide(n, 2))
    .Map(n => n * 2)
    .MapError(e => e.ToUpper())
    .Do(n => Console.WriteLine($"Result: {n}"))
    .DoIfError(e => Console.WriteLine($"Error: {e}"));

Unboxing

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

// Fold with handlers for both cases
string message = result.Fold(
    error => $"failed: {error}",
    success => $"got {success}");

// Get the value or a default derived from the error
int value = result.DefaultWith(error => -1);

// Get the value or throw
int certain = result.OrThrow();

// Convert to Option (error becomes None)
Option<int> option = result.ToOption();

The Try Monad

The Try<TSuccess> monad is a specialization of Result<Exception, TSuccess> that catches exceptions automatically via its Try.Catching constructor.

Constructing

// Catching exceptions automatically
var tried = Try.Catching(() => int.Parse("42"));
var failed = Try.Catching<int>(() => throw new Exception("boom"));

// Using factory methods
var success = Try<int>.Success(42);
var error = Try<int>.Exception(new InvalidOperationException("oops"));

// Using the non-generic static class
var success2 = Try.Success(42);
var error2 = Try.Exception<int>(new InvalidOperationException("oops"));

// Using extension methods
var success3 = 42.ToSuccess<int>();
var error3 = new InvalidOperationException("oops").ToException<int>();

Monadic operations

var result = Try.Catching(() => "42")
    .Map(int.Parse)
    .Bind(n => n > 0
        ? Try<int>.Success(n)
        : Try<int>.Exception(new Exception("must be positive")))
    .Do(n => Console.WriteLine($"Result: {n}"))
    .DoIfError(ex => Console.WriteLine($"Error: {ex.Message}"));

// MapCatching and BindCatching catch exceptions thrown by the callback
var safe = Try.Catching(() => "hello")
    .MapCatching(s => int.Parse(s))
    .BindCatching(n => Try<string>.Success(n.ToString()));

Unboxing

Try<TSuccess> inherits all unboxing methods from Result<Exception, TSuccess>:

var tried = Try.Catching(() => int.Parse("42"));

string message = tried.Fold(
    ex => $"failed: {ex.Message}",
    value => $"got {value}");

int value = tried.DefaultWith(ex => -1);
int certain = tried.OrThrow();

The Either Monad

The Either<TLeft, TRight> monad is a general-purpose discriminated union where both sides carry meaning. It is right-biased for chainable pipelines, but without the opinionated error/success semantics of Result. Use Either when both outcomes are valid domain values.

Constructing

// Using factory methods
var left = Either<string, int>.FromLeft("heads");
var right = Either<string, int>.FromRight(42);

// Using the non-generic static class
var left2 = Either.FromLeft<string, int>("heads");
var right2 = Either.FromRight<string, int>(42);

// Using extension methods
var left3 = "heads".ToLeft<string, int>();
var right3 = 42.ToRight<string, int>();

Monadic operations

var either = Either<string, int>.FromRight(21);

var result = either
    .Map(n => n * 2)
    .Bind(n => n > 0
        ? Either<string, int>.FromRight(n)
        : Either<string, int>.FromLeft("non-positive"))
    .MapLeft(msg => msg.ToUpper())
    .Do(n => Console.WriteLine($"Right: {n}"))
    .DoIfLeft(msg => Console.WriteLine($"Left: {msg}"));

Unboxing

var either = Either<string, int>.FromRight(42);

// Fold with handlers for both cases
string message = either.Fold(
    left => $"left: {left}",
    right => $"right: {right}");

// Get the right value or a default derived from the left value
int value = either.DefaultWith(left => -1);

// Get the right value or throw
int certain = either.OrThrow();

// Swap left and right sides
Either<int, string> swapped = either.Swap();

// Convert to Option (left becomes None) or Result (left becomes Error)
Option<int> option = either.ToOption();
Result<string, int> result = either.ToResult();

Async Support

Support of async workflows is added as a small set of extension methods. Sync operations work naturally after an await, and only callbacks that are themselves async need async-specific methods.

Awaiting then chaining sync operations

When an async function returns an Option<T>, Result<TError, T>, Try<T>, or Either<TLeft, TRight>, you can await it and then chain any sync operation as usual:

async Task<Option<int>> FindUserId(string username) { ... }

var result = (await FindUserId("malin"))
    .Map(id => id * 2)
    .Filter(id => id > 0)
    .DefaultWith(() => -1);

BindAsync and MapAsync

When the callback itself is async, BindAsync and MapAsync enable fluent chaining without intermediate awaits:

async Task<Option<string>> FindUserEmail(int userId) { ... }
async Task<string> NormalizeEmail(string email) { ... }

var result = await FindUserId("malin")
    .BindAsync(id => FindUserEmail(id))
    .MapAsync(email => NormalizeEmail(email));

These work the same way on Result, Try, and Either:

async Task<Result<string, int>> ParseUserId(string input) { ... }
async Task<Result<string, string>> LookupUsername(int userId) { ... }

var greeting = (await ParseUserId("42")
        .BindAsync(id => LookupUsername(id)))
    .Map(name => $"Welcome, {name}!")
    .DefaultWith(error => $"Error: {error}");
async Task<Either<string, int>> ParseId(string input) { ... }
async Task<Either<string, string>> LookupName(int id) { ... }

var greeting = (await ParseId("42")
        .BindAsync(id => LookupName(id)))
    .Map(name => $"Welcome, {name}!")
    .DefaultWith(left => $"Left: {left}");

Sequence

When you have a sync monad and Map it with an async function, you get e.g. Option<Task<T>>. Sequence flips this into Task<Option<T>> so you can await it:

Option<string> email = Option<string>.Some("  ALICE@EXAMPLE.COM  ");

var result = await email
    .Map(e => NormalizeEmail(e))
    .Sequence();

This also works on Result, Try, and Either, skipping the async work when in the error/left state.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

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
2.0.3 100 2/24/2026
2.0.2 93 2/18/2026
2.0.1 91 2/18/2026
1.31.3 103 2/16/2026
1.31.2 96 2/13/2026
1.30.1 111 2/12/2026
1.29.0 1,258 9/29/2025
1.28.0 273 9/29/2025

## What's Changed
* Upgrade to .NET 10 and enrich XML documentation with examples by @svan-jansson in https://github.com/svan-jansson/Svan.Monads/pull/10


**Full Changelog**: https://github.com/svan-jansson/Svan.Monads/compare/v2.0.2...v2.0.3