Functional programming library for .NET with Option, Result, Validation, Try, RemoteData, NonEmptyList, Writer, Reader, State, and IO monads. Features async/await support with CancellationToken, LINQ query syntax, zero dependencies on .NET 6+ (minimal polyfills on netstandard2.x), Roslyn analyzers, source generators for discriminated unions, ASP.NET Core and EF Core integrations.
$ dotnet add package Monad.NETMonad.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
Version 2.0 is a major release focused on C#-idiomatic naming, API simplification, and cross-type consistency:
Unwrap() to GetValue(), FlatMap to Bind, MapErr to MapErrorMap/Bind/Filter directly for clearer semanticsOk()/Error() across all typesdefault struct protection: Throws InvalidOperationException to prevent invalid statesAll core functionality remains — removed methods have straightforward replacements using Match(), GetValueOr(), and Map()/Bind().
See the full migration guide →
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.
Option<T> vs Nullable Reference TypesModern 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.
Result<T, E> vs ExceptionsModern 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<T, E> vs FluentValidationWith 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.
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)
);
| 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> |
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 |
# 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
| 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.
var email = FindUser(id)
.Map(user => user.Email)
.Filter(email => email.Contains("@"))
.Bind(email => ValidateEmail(email));
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}"));
}
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).
[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
);
| Document | Description |
|---|---|
| Quick Start Guide | Get up and running in 5 minutes |
| NuGet Packages | All packages with version badges and installation instructions |
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 |
| 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 |
| 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 |
| 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 |
The examples/ folder contains an example application:
examples/Monad.NET.Examples — Interactive console app demonstrating all monad types with real-world patterns.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.
Further reading on functional programming and these patterns:
| 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. |
| 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. |
| 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 |
| 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. |
Monad.NET adds value in specific scenarios. Here's a decision framework:
| 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 |
| 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> |
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
Adopting Monad.NET requires the team to understand:
Map vs Bind (transformation vs chaining)Match vs direct value accessRecommendation: 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.
Yes! Use Option<T> for optional relationships and Result<T, E> for operations that might fail. See EF Core Integration.
Absolutely. See ASP.NET Core Integration.
Result and Validation?Result — Short-circuits on first error (like &&)Validation — Accumulates ALL errors (for showing multiple validation messages)Yes. All types are immutable readonly struct with no shared mutable state.
Contributions are welcome. Please read CONTRIBUTING.md for guidelines.
Development requirements:
git clone https://github.com/behrangmohseni/Monad.NET.git
cd Monad.NET
dotnet build
dotnet test
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