Monad.NET
2.0.0-beta.3
dotnet add package Monad.NET --version 2.0.0-beta.3
NuGet\Install-Package Monad.NET -Version 2.0.0-beta.3
<PackageReference Include="Monad.NET" Version="2.0.0-beta.3" />
<PackageVersion Include="Monad.NET" Version="2.0.0-beta.3" />
<PackageReference Include="Monad.NET" />
paket add Monad.NET --version 2.0.0-beta.3
#r "nuget: Monad.NET, 2.0.0-beta.3"
#:package Monad.NET@2.0.0-beta.3
#addin nuget:?package=Monad.NET&version=2.0.0-beta.3&prerelease
#tool nuget:?package=Monad.NET&version=2.0.0-beta.3&prerelease
Monad.NET
Monad.NET is a functional programming library for .NET. Option, Result, Validation, Try, and more — with zero dependencies on .NET 6+.
// Transform nullable chaos into composable clarity
var result = user.ToOption()
.Filter(u => u.IsActive)
.Map(u => u.Email)
.Bind(email => SendWelcome(email))
.Match(
some: _ => "Email sent",
none: () => "User not found or inactive"
);
Author: Behrang Mohseni
License: MIT — Free for commercial and personal use
Upgrading to v2.0
Version 2.0 is a major release focused on C#-idiomatic naming, API simplification, and cross-type consistency:
- ~285 methods removed for better discoverability
- Rust-style to C#-style naming:
Unwrap()toGetValue(),FlatMaptoBind,MapErrtoMapError - LINQ support removed: Use
Map/Bind/Filterdirectly for clearer semantics - Consistent factory methods:
Ok()/Error()across all types defaultstruct protection: ThrowsInvalidOperationExceptionto prevent invalid states- 2,035 tests ensure correctness across all changes
All core functionality remains — removed methods have straightforward replacements using Match(), GetValueOr(), and Map()/Bind().
See the full migration guide →
Table of Contents
- Why Monad.NET?
- Which Monad Should I Use?
- Installation
- Quick Start
- Documentation
- Examples
- Performance
- Resources
- FAQ
- Contributing
Why Monad.NET?
Modern C# has excellent features—nullable reference types, pattern matching, records. So why use Monad.NET?
The short answer: Composability. While C# handles individual cases well, chaining operations that might fail, be absent, or need validation quickly becomes verbose. Monad.NET provides a unified API for composing these operations elegantly.
Honest Comparisons with Modern C#
Optional Values: Option<T> vs Nullable Reference Types
Modern C# (NRT enabled):
User? user = FindUser(id);
if (user is not null)
{
Profile? profile = user.GetProfile();
if (profile is not null)
{
return profile.Email; // Still might be null!
}
}
return "default@example.com";
With Monad.NET:
return FindUser(id)
.Bind(user => user.GetProfile())
.Map(profile => profile.Email)
.GetValueOr("default@example.com");
Verdict: NRTs catch null issues at compile time—use them! But Option<T> shines when you need to chain operations or transform optional values. If you're writing nested null checks, Option is cleaner.
Error Handling: Result<T, E> vs Exceptions
Modern C# with exceptions:
public Order ProcessOrder(OrderRequest request)
{
try
{
var validated = ValidateOrder(request); // throws ValidationException
var inventory = ReserveInventory(validated); // throws InventoryException
var payment = ChargePayment(inventory); // throws PaymentException
return CreateOrder(payment);
}
catch (ValidationException ex) { /* handle */ }
catch (InventoryException ex) { /* handle */ }
catch (PaymentException ex) { /* handle */ }
}
With Monad.NET:
public Result<Order, OrderError> ProcessOrder(OrderRequest request)
{
return ValidateOrder(request)
.Bind(ReserveInventory)
.Bind(ChargePayment)
.Bind(CreateOrder);
}
Verdict: Exceptions are fine for exceptional situations (network failures, disk errors). Use Result<T, E> when failure is expected (validation errors, business rule violations). The signature Result<Order, OrderError> tells callers exactly what can go wrong—no surprises.
Validation: Validation<T, E> vs FluentValidation
With FluentValidation (industry standard):
public class UserValidator : AbstractValidator<UserRequest>
{
public UserValidator()
{
RuleFor(x => x.Name).NotEmpty().MinimumLength(2);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Age).InclusiveBetween(18, 120);
}
}
// Usage
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
return BadRequest(result.Errors);
With Monad.NET:
var user = ValidateName(request.Name)
.Apply(ValidateEmail(request.Email), (name, email) => (name, email))
.Apply(ValidateAge(request.Age), (partial, age) => new User(partial.name, partial.email, age));
Verdict: FluentValidation is battle-tested and has more features (async rules, dependency injection, localization). Use it for complex scenarios. Validation<T, E> is lighter, has no dependencies, and works well with other Monad.NET types. Choose based on your needs.
Discriminated Unions: The Missing Feature in C#
The Problem: C# still lacks native discriminated unions (sum types) as of C# 14. Despite adding extension members, null-conditional assignment, field-backed properties, and other features—discriminated unions didn't make the cut. This remains one of the most requested language features, with the proposal actively discussed by the C# Language Design Team. F#, Rust, Swift, Kotlin, and TypeScript all have this feature. C# developers have been waiting for years.
With Monad.NET Source Generators:
[Union]
public abstract partial record GetUserResult
{
public partial record Success(User User) : GetUserResult;
public partial record NotFound : GetUserResult;
public partial record ValidationError(string Message) : GetUserResult;
}
// Exhaustive matching - compiler ensures all cases handled
result.Match(
success: s => Ok(s.User),
notFound: _ => NotFound(),
validationError: e => BadRequest(e.Message)
);
Design Principles
- Explicit over implicit — No hidden nulls, no surprise exceptions
- Composition over inheritance — Small, focused types that combine well
- Immutability by default — All types are immutable and thread-safe
- Minimal dependencies — Zero on .NET 6+; only Microsoft polyfills on netstandard2.x
Which Monad Should I Use?
| Scenario | Use This |
|---|---|
| A value might be missing | Option<T> |
| An operation can fail with a typed error | Result<T, E> |
| Need to show ALL validation errors at once | Validation<T, E> |
| Wrapping code that throws exceptions | Try<T> |
| A list must have at least one item | NonEmptyList<T> |
| UI state for async data loading (Blazor) | RemoteData<T, E> |
| Dependency injection without DI container | Reader<R, A> |
| Need to accumulate logs/traces alongside results | Writer<W, T> |
| Thread state through pure computations | State<S, A> |
| Defer and compose side effects | IO<T> |
Language Inspirations
These types come from functional programming languages. Here's the lineage:
| Monad.NET | F# | Rust | Haskell |
|---|---|---|---|
Option<T> |
Option<'T> |
Option<T> |
Maybe a |
Result<T,E> |
Result<'T,'E> |
Result<T,E> |
Either a b |
Validation<T,E> |
— | — | Validation e a |
Try<T> |
— | — | — (Scala) |
RemoteData<T,E> |
— | — | — (Elm) |
NonEmptyList<T> |
— | — | NonEmpty a |
Writer<W,T> |
— | — | Writer w a |
Reader<R,A> |
— | — | Reader r a |
State<S,A> |
— | — | State s a |
IO<T> |
— | — | IO a |
Installation
# Core library
dotnet add package Monad.NET
# Optional: Discriminated unions via source generators
dotnet add package Monad.NET.SourceGenerators
# Optional: ASP.NET Core integration
dotnet add package Monad.NET.AspNetCore
# Optional: Entity Framework Core integration
dotnet add package Monad.NET.EntityFrameworkCore
Dependencies
| Target Framework | Dependencies |
|---|---|
| .NET 6.0+ | None (zero dependencies) |
| .NET Standard 2.1 | Microsoft.Bcl.AsyncInterfaces, System.Collections.Immutable, System.Text.Json |
| .NET Standard 2.0 | Above + System.Memory |
Note: The netstandard2.x dependencies are Microsoft polyfill packages that provide modern .NET APIs to older frameworks. They are automatically included and have no transitive third-party dependencies.
Quick Start
Option — Handle missing values
var email = FindUser(id)
.Map(user => user.Email)
.Filter(email => email.Contains("@"))
.Bind(email => ValidateEmail(email));
Result — Handle expected failures
public Result<Order, OrderError> ProcessOrder(OrderRequest request)
{
return ValidateOrder(request)
.Bind(order => CheckInventory(order))
.Bind(order => ChargePayment(order))
.Tap(order => _logger.LogInfo($"Order {order.Id} created"))
.TapError(err => _logger.LogError($"Order failed: {err}"));
}
Validation — Collect all errors
var user = ValidateName(form.Name)
.Apply(ValidateEmail(form.Email), (name, email) => (name, email))
.Apply(ValidateAge(form.Age), (partial, age) => new User(partial.name, partial.email, age));
// Shows ALL validation errors at once
user.Match(
valid: u => CreateUser(u),
invalid: errors => ShowErrors(errors)
);
Tip: Use
Apply()orZip()to accumulate all errors.Bind()short-circuits on the first error (useResultif you only need the first error).
Discriminated Unions — Type-safe alternatives
[Union]
public abstract partial record Shape
{
public partial record Circle(double Radius) : Shape;
public partial record Rectangle(double Width, double Height) : Shape;
}
// Exhaustive matching
var area = shape.Match(
circle: c => Math.PI * c.Radius * c.Radius,
rectangle: r => r.Width * r.Height
);
Documentation
Getting Started
| Document | Description |
|---|---|
| Quick Start Guide | Get up and running in 5 minutes |
| NuGet Packages | All packages with version badges and installation instructions |
Learning Functional Programming
New to functional programming? Start here:
| Document | Description |
|---|---|
| Why Functional Error Handling? | The case for Result over exceptions |
| Railway-Oriented Programming | The mental model for error handling |
| Option Explained | Handling missing values safely |
| Result Explained | Handling operations that can fail |
| Composition Patterns | Building complex operations from simple ones |
| From OOP to FP | Mental shift guide for C# developers |
Reference Documentation
| Document | Description |
|---|---|
| Core Types | Detailed docs for Option, Result, Validation, Try, and more |
| Advanced Usage | Async operations, collection methods, parallel processing |
| Examples | Real-world code samples |
| Integrations | Source Generators, ASP.NET Core, Entity Framework Core |
| API Reference | Complete API documentation |
Guides
| Document | Description |
|---|---|
| Pitfalls & Gotchas | Common mistakes to avoid |
| Async Patterns | How to use async/await with monadic types |
| Logging Guidance | Best practices for logging |
| Type Selection Guide | Decision flowchart for choosing the right type |
| Migration Guide | Migrate from language-ext, OneOf, FluentResults |
Project Information
| Document | Description |
|---|---|
| Compatibility | Supported .NET versions |
| Performance Benchmarks | Detailed performance comparisons and analysis |
| Versioning Policy | API versioning and deprecation policy |
| Architectural Decisions | Design decisions, rationale, and trade-offs |
Examples
The examples/ folder contains an example application:
examples/Monad.NET.Examples— Interactive console app demonstrating all monad types with real-world patterns.
Performance
Monad.NET is designed for correctness and safety first, but performance is still a priority:
| Aspect | Details |
|---|---|
| Struct-based | Option<T>, Result<T,E>, Try<T>, etc. are readonly struct — no heap allocations |
| No boxing | Generic implementations avoid boxing value types |
| Lazy evaluation | Match, OrElse use Func<> for deferred computation |
| Zero allocations | Most operations on value types are allocation-free |
| Aggressive inlining | Hot paths use [MethodImpl(AggressiveInlining)] |
| ConfigureAwait(false) | All async methods use ConfigureAwait(false) |
For typical use cases, the overhead is negligible (nanoseconds). The safety guarantees and code clarity typically outweigh any micro-optimization concerns.
Resources
Further reading on functional programming and these patterns:
Books
| Book | Author | Why Read It |
|---|---|---|
| Functional Programming in C# | Enrico Buonanno | The definitive guide to FP in C#. Covers Option, Result, Validation, and more. |
| Domain Modeling Made Functional | Scott Wlaschin | Uses F# but concepts translate directly. Excellent on making illegal states unrepresentable. |
| Programming Rust | Blandy, Orendorff, Tindall | Rust's Option and Result are nearly identical to Monad.NET's versions. |
Online Resources
| Resource | Description |
|---|---|
| F# for Fun and Profit | Scott Wlaschin's legendary site. Start with Railway Oriented Programming. |
| Rust Error Handling | Official Rust book chapter on Option and Result. |
| Haskell Error Handling | Haskell wiki on error handling patterns. |
| Parse, Don't Validate | Alexis King's influential post on type-driven design. |
Videos & Talks
| Talk | Speaker | Topics |
|---|---|---|
| Functional Design Patterns | Scott Wlaschin | Monads, Railway Oriented Programming, composition |
| Domain Modeling Made Functional | Scott Wlaschin | Making illegal states unrepresentable |
| The Power of Composition | Scott Wlaschin | Why small, composable functions matter |
Related C# Libraries
| Library | Description |
|---|---|
| language-ext | Extensive FP library for C#. More features than Monad.NET but steeper learning curve. |
| OneOf | Focused on discriminated unions. Lighter weight. |
| FluentResults | Result pattern with fluent API. Good for simple use cases. |
| ErrorOr | Discriminated union for errors. Popular in Clean Architecture circles. |
Key Concepts
- Railway Oriented Programming — Treat errors as alternate tracks, not exceptions
- Making Illegal States Unrepresentable — Use types to prevent bugs at compile time
- Parse, Don't Validate — Push validation to the boundaries, work with valid types internally
- Composition over Inheritance — Small, focused types that combine well
When NOT to Use Monad.NET
Monad.NET adds value in specific scenarios. Here's a decision framework:
Use Native C# When...
| Scenario | Why | What to Use Instead |
|---|---|---|
| Single null check | Monad overhead isn't justified | x?.Property ?? default |
| Truly exceptional errors | Stack traces are valuable for debugging | try/catch with proper exception types |
| Performance-critical inner loops | Lambda allocations matter at scale | Traditional if/else, early returns |
| Simple CRUD with no composition | No chaining benefit | Direct calls, simple conditionals |
Use Monad.NET When...
| Scenario | Why | What to Use |
|---|---|---|
| Chaining 3+ nullable operations | Avoids nested if (x != null) pyramids |
Option<T> with Bind/Map |
| Expected failures with typed errors | Error handling in the type signature | Result<T, E> |
| Showing ALL validation errors | Accumulation instead of short-circuit | Validation<T, E> |
| Wrapping exception-throwing code | Convert exceptions to values | Try<T> |
| UI async state (loading/error/success) | Explicit state modeling | RemoteData<T, E> |
Decision Flowchart
Is the value optional?
├─► YES: How many operations do you chain?
│ ├─► 1-2: Use nullable reference types (T?)
│ └─► 3+: Use Option<T>
│
└─► NO: Can the operation fail?
├─► YES: Is failure exceptional (bugs, network, disk)?
│ ├─► YES: Use exceptions
│ └─► NO: Is it validation?
│ ├─► YES: Need ALL errors? Use Validation<T,E>
│ │ First error is enough? Use Result<T,E>
│ └─► NO: Use Result<T,E>
└─► NO: Just use regular types
Team Considerations
Adopting Monad.NET requires the team to understand:
MapvsBind(transformation vs chaining)- Railway-oriented programming concepts
- When to use
Matchvs direct value access
Recommendation: Start with Option<T> and Result<T,E> only. Add other types as the team gains familiarity. Don't adopt the full library at once.
FAQ
Can I use Monad.NET with Entity Framework?
Yes! Use Option<T> for optional relationships and Result<T, E> for operations that might fail. See EF Core Integration.
Can I use Monad.NET with ASP.NET Core?
Absolutely. See ASP.NET Core Integration.
What's the difference between Result and Validation?
Result— Short-circuits on first error (like&&)Validation— Accumulates ALL errors (for showing multiple validation messages)
Is Monad.NET thread-safe?
Yes. All types are immutable readonly struct with no shared mutable state.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md for guidelines.
Development requirements:
- .NET 8.0 SDK or later (for building all targets)
- 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 | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. 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 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 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 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 is compatible. |
| .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. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- System.Collections.Immutable (>= 8.0.0)
- System.Memory (>= 4.5.5)
- System.Text.Json (>= 8.0.5)
-
.NETStandard 2.1
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- System.Collections.Immutable (>= 8.0.0)
- System.Text.Json (>= 8.0.5)
-
net6.0
- No dependencies.
-
net8.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.3 | 48 | 2/23/2026 |
| 2.0.0-beta.2 | 51 | 2/2/2026 |
| 2.0.0-beta.1 | 51 | 2/1/2026 |
| 1.1.2 | 177 | 1/25/2026 |
| 1.1.1 | 174 | 1/7/2026 |
| 1.1.0 | 181 | 12/30/2025 |
| 1.0.0 | 181 | 12/28/2025 |
| 1.0.0-beta.2 | 143 | 12/24/2025 |
| 1.0.0-beta.1 | 149 | 12/23/2025 |
| 1.0.0-alpha.13 | 154 | 12/22/2025 |
| 1.0.0-alpha.11 | 93 | 12/21/2025 |
| 1.0.0-alpha.10 | 243 | 12/16/2025 |
| 1.0.0-alpha.9 | 238 | 12/16/2025 |
| 1.0.0-alpha.8 | 213 | 12/15/2025 |
| 1.0.0-alpha.7 | 197 | 12/15/2025 |
| 1.0.0-alpha.5 | 183 | 12/15/2025 |
| 1.0.0-alpha.4 | 120 | 12/14/2025 |
| 1.0.0-alpha.3 | 117 | 12/14/2025 |
| 1.0.0-alpha.2 | 83 | 12/13/2025 |
| 1.0.0-alpha.1 | 97 | 12/13/2025 |