Monad.NET.EntityFrameworkCore 2.0.0-beta.2

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

Monad.NET

NuGet NuGet Downloads Build CodeQL codecov CodeFactor License: MIT .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 and API simplification:

  • ~285 methods removed for better discoverability
  • Rust-style → C#-style naming: Unwrap()GetValue(), FlatMapBind, MapErrMapError
  • default(Result<T,E>) now protected: Throws InvalidOperationException to prevent invalid states
  • 2,042 tests ensure correctness across all changes

All core functionality remains — removed methods have straightforward replacements using Match(), GetValueOr(), and LINQ.

See the full migration guide →


Table of Contents


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.IsOk)
    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

  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. 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>
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>

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
ReaderAsync<R,A> ReaderT IO 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

// 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("@"))
    .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"))
        .TapErr(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)
);

Important: LINQ query syntax (from...select) on Validation short-circuits on the first error. Use Apply() or Zip() to accumulate all errors.

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

Document Description
NuGet Packages All packages with version badges and installation instructions
Quick Start Guide Get up and running in 5 minutes
Core Types Detailed docs for Option, Result, 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
Architectural Decisions Design decisions, rationale, and trade-offs

Examples

The examples/ folder contains a comprehensive 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 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.


Resources

Want to dive deeper into 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
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

  1. Railway Oriented Programming — Treat errors as alternate tracks, not exceptions
  2. Making Illegal States Unrepresentable — Use types to prevent bugs at compile time
  3. Parse, Don't Validate — Push validation to the boundaries, work with valid types internally
  4. Composition over Inheritance — Small, focused types that combine well

When NOT to Use Monad.NET

Monad.NET is not always the right choice. Here's when to stick with native C#:

Scenario Recommendation
Simple null checks Use ??, ?., and nullable reference types
Exceptional failures (IO, network) Use exceptions — they're designed for this
Performance-critical hot loops Avoid lambda allocations; use traditional control flow
Team unfamiliar with FP concepts Consider the learning curve before adoption
Simple CRUD operations Often overkill; use when composition benefits outweigh complexity

Good rule of thumb: If you're writing if (x != null) { ... } once, use nullable. If you're chaining multiple such checks, use Option.


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

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.0-beta.2 44 2/2/2026
2.0.0-beta.1 37 2/1/2026
1.1.2 83 1/25/2026
1.1.1 90 1/7/2026
1.1.0 94 12/30/2025
1.0.0 93 12/28/2025
1.0.0-beta.2 134 12/24/2025
1.0.0-beta.1 128 12/23/2025
1.0.0-alpha.13 128 12/22/2025
1.0.0-alpha.11 74 12/21/2025
1.0.0-alpha.10 225 12/16/2025
1.0.0-alpha.9 220 12/16/2025
1.0.0-alpha.8 194 12/15/2025