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.
Monad.NET is a functional programming library for .NET. Option, Result, Either, Validation, and more — with zero dependencies.
// 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
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)
.AndThen(user => user.GetProfile())
.Map(profile => profile.Email)
.UnwrapOr("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)
.AndThen(ReserveInventory)
.AndThen(ChargePayment)
.AndThen(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> |
| Return one of two different types | Either<L, R> |
| Compose async operations with shared dependencies | ReaderAsync<R, A> |
| 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 |
Either<L,R> | Choice<'T1,'T2> | — | 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 |
ReaderAsync<R,A> | — | — | ReaderT IO 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// Method syntax (recommended)
var email = FindUser(id)
.Select(user => user.Email)
.Where(email => email.Contains("@"))
.SelectMany(email => ValidateEmail(email));
// Or use Map/Filter/AndThen
var email = FindUser(id)
.Map(user => user.Email)
.Filter(email => email.Contains("@"))
.AndThen(email => ValidateEmail(email));public Result<Order, OrderError> ProcessOrder(OrderRequest request)
{
return ValidateOrder(request)
.AndThen(order => CheckInventory(order))
.AndThen(order => ChargePayment(order))
.Tap(order => _logger.LogInfo($"Order {order.Id} created"))
.TapErr(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)
);[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 |
| Core Types | Detailed docs for Option, Result, Either, Validation, Try, and more |
| Advanced Usage | LINQ, async, collection operations, parallel processing |
| Examples | Real-world code samples |
| Integrations | Source Generators, ASP.NET Core, Entity Framework Core |
| API Reference | Complete API documentation |
| Compatibility | Supported .NET versions |
| Performance Benchmarks | Detailed performance comparisons and analysis |
| Versioning Policy | API versioning and deprecation policy |
| Pitfalls & Gotchas | Common mistakes to avoid |
| Logging Guidance | Best practices for logging |
| Type Selection Guide | Decision flowchart for choosing the right type |
| Migration Guide | Migrate from language-ext, OneOf, FluentResults |
The examples/ folder contains runnable samples:
examples/Monad.NET.Samples — Console app demonstrating Option, Result, Validation, Writer, RemoteData, and IO.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 | UnwrapOrElse, 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.
Want to dive deeper into 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, Either, 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 Maybe/Either | 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. |
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 Either?Result<T, E> — Semantically means success or failure. Right-biased (operations work on Ok).Either<L, R> — General "one of two types" with no success/failure implication. Can work on either side.Use Result for error handling. Use Either when both sides are valid outcomes.
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 testThis 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