Monad.NET 1.0.0-alpha.2

This is a prerelease version of Monad.NET.
There is a newer version of this package available.
See the version list below for details.
dotnet add package Monad.NET --version 1.0.0-alpha.2
                    
NuGet\Install-Package Monad.NET -Version 1.0.0-alpha.2
                    
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="Monad.NET" Version="1.0.0-alpha.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Monad.NET" Version="1.0.0-alpha.2" />
                    
Directory.Packages.props
<PackageReference Include="Monad.NET" />
                    
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 Monad.NET --version 1.0.0-alpha.2
                    
#r "nuget: Monad.NET, 1.0.0-alpha.2"
                    
#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 Monad.NET@1.0.0-alpha.2
                    
#: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=Monad.NET&version=1.0.0-alpha.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Monad.NET&version=1.0.0-alpha.2&prerelease
                    
Install as a Cake Tool

Monad.NET

NuGet NuGet Downloads License: MIT .NET

Monad.NET is a functional programming library for .NET that provides a robust set of monadic types for building reliable, composable, and maintainable applications.

// Transform nullable chaos into composable clarity
var result = user.ToOption()
    .Filter(u => u.IsActive)
    .Map(u => u.Email)
    .AndThen(email => SendWelcome(email))
    .Match(
        some: _ => "Email sent",
        none: () => "User not found or inactive"
    );

Author: Behrang Mohseni
License: MIT — Free for commercial and personal use


Table of Contents


Why Monad.NET?

Modern .NET applications demand reliability. Yet we continue to fight the same battles: null reference exceptions, swallowed errors, inconsistent error handling, and code that's difficult to reason about.

Monad.NET addresses these challenges:

Problem Traditional Approach Monad.NET Solution
Null references if (x != null) checks scattered everywhere Option<T> makes absence explicit and composable
Error handling Try-catch blocks, exceptions as control flow Result<T, E> treats errors as data
Validation Return on first error, lose context Validation<T, E> accumulates all errors
Async state Boolean flags (isLoading, hasError) RemoteData<T, E> models all four states
Empty collections Runtime exceptions on .First() NonEmptyList<T> guarantees at least one element

Design Principles

  1. Explicit over implicit — No hidden nulls, no surprise exceptions
  2. Composition over inheritance — Small, focused types that combine well
  3. Immutability by default — All types are immutable and thread-safe
  4. Zero dependencies — Only the .NET runtime, nothing else

Installation

Requires .NET 6.0 or later.

dotnet add package Monad.NET --version 1.0.0-alpha.1

Package Manager Console:

Install-Package Monad.NET -Version 1.0.0-alpha.1

PackageReference:

<PackageReference Include="Monad.NET" Version="1.0.0-alpha.1" />

Quick Start

using Monad.NET;

// Option: Handle missing values without null
Option<User> FindUser(int id) => 
    _users.TryGetValue(id, out var user) 
        ? Option<User>.Some(user) 
        : Option<User>.None();

var greeting = FindUser(42)
    .Map(u => u.Name)
    .Map(name => $"Hello, {name}!")
    .UnwrapOr("Hello, guest!");

// Result: Explicit error handling
Result<Order, OrderError> PlaceOrder(Cart cart)
{
    if (cart.IsEmpty)
        return Result<Order, OrderError>.Err(OrderError.EmptyCart);
    
    if (!cart.HasValidPayment)
        return Result<Order, OrderError>.Err(OrderError.InvalidPayment);
    
    return Result<Order, OrderError>.Ok(new Order(cart));
}

var outcome = PlaceOrder(cart)
    .Map(order => order.Id)
    .Match(
        ok: id => $"Order #{id} placed successfully",
        err: error => $"Failed: {error}"
    );

// Validation: Collect ALL errors at once
var registration = ValidateUsername(form.Username)
    .Apply(ValidateEmail(form.Email), (u, e) => (u, e))
    .Apply(ValidatePassword(form.Password), (partial, p) => 
        new Registration(partial.u, partial.e, p));

registration.Match(
    valid: reg => CreateAccount(reg),
    invalid: errors => ShowErrors(errors) // Shows ALL validation errors
);

Core Types

Option<T>

Represents a value that may or may not exist. Use instead of null.

// Creation
var some = Option<int>.Some(42);
var none = Option<int>.None();
var fromNullable = possiblyNull.ToOption();  // Extension method

// Transformation
var doubled = some.Map(x => x * 2);                    // Some(84)
var filtered = some.Filter(x => x > 100);              // None
var chained = some.AndThen(x => LookupValue(x));       // Chains Option-returning functions

// Extraction
var value = some.UnwrapOr(0);                          // 42
var computed = none.UnwrapOrElse(() => ComputeDefault()); // Lazy evaluation

// Pattern matching
var message = some.Match(
    some: v => $"Found: {v}",
    none: () => "Not found"
);

When to use: Any time you would return null or use Nullable<T>.


Result<T, E>

Represents either success (Ok) or failure (Err) with a typed error.

// Creation
var ok = Result<int, string>.Ok(42);
var err = Result<int, string>.Err("Something went wrong");

// Safe exception handling
var parsed = ResultExtensions.Try(() => int.Parse(input));
var fetched = await ResultExtensions.TryAsync(() => httpClient.GetAsync(url));

// Railway-oriented programming
var pipeline = ParseInput(raw)
    .AndThen(Validate)
    .AndThen(Transform)
    .AndThen(Save)
    .Tap(result => _logger.LogInformation("Saved: {Id}", result.Id))
    .TapErr(error => _logger.LogError("Failed: {Error}", error));

// Recovery strategies
var recovered = err.OrElse(e => FallbackStrategy(e));
var withDefault = err.UnwrapOr(defaultValue);

When to use: Operations that can fail with meaningful error information.


Either<L, R>

Represents a value of one of two types. More general than Result.

var right = Either<ValidationError, User>.Right(user);
var left = Either<ValidationError, User>.Left(error);

// Transform either side
var mapped = either.BiMap(
    left: err => err.Message,
    right: user => user.Id
);

// Swap sides
var swapped = either.Swap();

// Conversions
var asResult = either.ToResult();
var asOption = either.ToOption();  // Right → Some, Left → None

When to use: When both sides represent valid outcomes, not just success/failure.


Validation<T, E>

Unlike Result, validation accumulates all errors instead of short-circuiting.

Validation<string, ValidationError> ValidateEmail(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        return Validation<string, ValidationError>.Invalid(
            new ValidationError("Email", "Email is required"));
    
    if (!email.Contains('@'))
        return Validation<string, ValidationError>.Invalid(
            new ValidationError("Email", "Invalid email format"));
    
    return Validation<string, ValidationError>.Valid(email);
}

// Combine validations — errors accumulate!
var user = ValidateName(form.Name)
    .Apply(ValidateEmail(form.Email), (name, email) => (name, email))
    .Apply(ValidateAge(form.Age), (partial, age) => 
        new UserDto(partial.name, partial.email, age));

// All three can fail, and you'll see ALL errors
user.Match(
    valid: dto => CreateUser(dto),
    invalid: errors => 
    {
        foreach (var error in errors)
            Console.WriteLine($"{error.Field}: {error.Message}");
    }
);

When to use: Form validation, input validation, anywhere you need to show all errors at once.


Try<T>

Wraps computations that might throw, converting exceptions to values.

// Capture exceptions
var result = Try<int>.Of(() => int.Parse("not a number"));
// → Failure(FormatException)

var asyncResult = await Try<string>.OfAsync(() => 
    httpClient.GetStringAsync(url));

// Recovery
var recovered = result
    .Recover(ex => -1)                     // Returns Try<int>.Success(-1)
    .Map(x => x * 2);

// Filtering with custom exception
var positive = Try<int>.Of(() => int.Parse(input))
    .Filter(x => x > 0, "Value must be positive");

// Conversion
var asResult = result.ToResult(ex => ex.Message);
var asOption = result.ToOption();  // Success → Some, Failure → None

When to use: Interfacing with code that throws exceptions, parsing, I/O operations.


RemoteData<T, E>

Models the four states of asynchronous data: NotAsked, Loading, Success, Failure.

// State management
RemoteData<User, ApiError> userData = RemoteData<User, ApiError>.NotAsked();

async Task LoadUser(int userId)
{
    userData = RemoteData<User, ApiError>.Loading();
    StateHasChanged();
    
    try
    {
        var user = await _api.GetUserAsync(userId);
        userData = RemoteData<User, ApiError>.Success(user);
    }
    catch (ApiException ex)
    {
        userData = RemoteData<User, ApiError>.Failure(ex.Error);
    }
    
    StateHasChanged();
}

// Rendering
@userData.Match(
    notAsked: () => @<button @onclick="() => LoadUser(1)">Load User</button>,
    loading: () => @<div class="spinner">Loading...</div>,
    success: user => @<UserProfile User="@user" />,
    failure: error => @<ErrorDisplay Error="@error" OnRetry="() => LoadUser(1)" />
)

When to use: UI state for async operations, replacing boolean flag combinations.


NonEmptyList<T>

A list guaranteed to have at least one element. Head and Reduce are always safe.

// Creation
var list = NonEmptyList<int>.Of(1, 2, 3, 4, 5);
var single = NonEmptyList<int>.Of(42);

// From existing collection (returns Option)
var maybeList = NonEmptyList<int>.FromEnumerable(existingList);
// → Some(list) or None if empty

// Safe operations — no exceptions possible
var first = list.Head;                           // 1 (always exists)
var last = list.Last();                          // 5 (always exists)
var sum = list.Reduce((a, b) => a + b);          // 15 (no seed needed)

// Transformations
var doubled = list.Map(x => x * 2);
var expanded = list.FlatMap(x => NonEmptyList<int>.Of(x, x * 10));

// Filter returns Option (result might be empty)
var filtered = list.Filter(x => x > 10);         // None

When to use: When empty collections are invalid states (config items, selected options, etc.).


Writer<W, T>

Computations that produce a value alongside accumulated output (logs, traces, metrics).

// Computation with logging
var computation = Writer<List<string>, int>.Tell(1, new List<string> { "Started with 1" })
    .FlatMap(
        x => Writer<List<string>, int>.Tell(x * 2, new List<string> { $"Doubled to {x * 2}" }),
        (log1, log2) => log1.Concat(log2).ToList()
    )
    .FlatMap(
        x => Writer<List<string>, int>.Tell(x + 10, new List<string> { $"Added 10, result: {x + 10}" }),
        (log1, log2) => log1.Concat(log2).ToList()
    );

Console.WriteLine($"Result: {computation.Value}");  // 12
Console.WriteLine($"Log: {string.Join(" → ", computation.Log)}");
// Started with 1 → Doubled to 2 → Added 10, result: 12

When to use: Audit trails, computation tracing, accumulating metadata.


Reader<R, A>

Computations that depend on a shared environment. Functional dependency injection.

// Define your environment
public record AppServices(
    IUserRepository Users,
    IEmailService Email,
    ILogger Logger
);

// Build computations that depend on services
var sendWelcome = Reader<AppServices, Task>.From(async services =>
{
    var users = await services.Users.GetNewUsersAsync();
    
    foreach (var user in users)
    {
        await services.Email.SendWelcomeAsync(user.Email);
        services.Logger.LogInformation("Sent welcome to {Email}", user.Email);
    }
});

// Compose readers
var workflow = Reader<AppServices, string>.Asks(s => s.Users)
    .FlatMap(repo => Reader<AppServices, string>.From(async s =>
    {
        var count = await repo.CountAsync();
        s.Logger.LogInformation("Total users: {Count}", count);
        return $"Processed {count} users";
    }));

// Execute with environment
var services = new AppServices(userRepo, emailService, logger);
await sendWelcome.Run(services);

When to use: Passing configuration/services through call chains without parameter drilling.


Advanced Usage

LINQ Query Syntax

All monads support LINQ for natural composition:

// Option
var result = from user in FindUser(id)
             from profile in LoadProfile(user.Id)
             where profile.IsComplete
             select new UserView(user, profile);

// Result
var order = from cart in ValidateCart(input)
            from payment in ProcessPayment(cart)
            from confirmation in CreateOrder(cart, payment)
            select confirmation;

Async Extensions

Seamless async/await integration:

var result = await Option<int>.Some(userId)
    .MapAsync(async id => await _repo.FindAsync(id))
    .AndThenAsync(async user => await ValidateAsync(user))
    .MapAsync(async user => await EnrichAsync(user));

Collection Operations

Work with sequences of monads:

// Sequence: [Option<T>] → Option<[T]>
var options = new[] { Option<int>.Some(1), Option<int>.Some(2), Option<int>.Some(3) };
var sequenced = options.Sequence();  // Some([1, 2, 3])

// Traverse: Map + Sequence in one pass
var validated = userIds.Traverse(id => ValidateUser(id));

// Partition: Separate successes and failures
var results = items.Select(Process);
var (successes, failures) = results.Partition();

// Choose: Filter and unwrap
var values = options.Choose();  // Only the Some values

API Reference

Full API documentation is available in docs/API.md.


Contributing

Contributions are welcome. Please read CONTRIBUTING.md for guidelines.

Development requirements:

  • .NET 8.0 SDK or later
  • Your preferred IDE (Visual Studio, Rider, VS Code)
git clone https://github.com/behrangmohseni/Monad.NET.git
cd Monad.NET
dotnet build
dotnet test

License

This project is licensed under the MIT License.

You are free to use, modify, and distribute this library in both commercial and open-source projects. See LICENSE for details.


Monad.NET — Functional programming for the pragmatic .NET developer.

Documentation · NuGet · Issues

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  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 is compatible.  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 is compatible.  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 is compatible.  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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net7.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on Monad.NET:

Package Downloads
Monad.NET.AspNetCore

ASP.NET Core integration for Monad.NET. IActionResult extensions and middleware for Option, Result, Validation, and Try types.

Monad.NET.EntityFrameworkCore

Entity Framework Core integration for Monad.NET - Option<T> property support and query extensions

Monad.NET.MessagePack

MessagePack serialization support for Monad.NET types. High-performance binary serialization for Option, Result, Try, Validation, and other monad types.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0-beta.2 45 2/2/2026
2.0.0-beta.1 45 2/1/2026
1.1.2 166 1/25/2026
1.1.1 163 1/7/2026
1.1.0 172 12/30/2025
1.0.0 173 12/28/2025
1.0.0-beta.2 137 12/24/2025
1.0.0-beta.1 142 12/23/2025
1.0.0-alpha.13 146 12/22/2025
1.0.0-alpha.11 85 12/21/2025
1.0.0-alpha.10 233 12/16/2025
1.0.0-alpha.9 231 12/16/2025
1.0.0-alpha.8 205 12/15/2025
1.0.0-alpha.7 190 12/15/2025
1.0.0-alpha.5 175 12/15/2025
1.0.0-alpha.4 112 12/14/2025
1.0.0-alpha.3 109 12/14/2025
1.0.0-alpha.2 76 12/13/2025
1.0.0-alpha.1 86 12/13/2025