Introduction to Functionally Library for .NET
If you have ever chased a NullReferenceException through five layers of call stack, wrapped half your service in try/catch just to translate exceptions into HTTP status codes, or lost the thread of a method buried in nested if checks, this library is for you. Functionally (aka D20Tek.Functional) gives you the functional building blocks C# still lacks, so you can write code where absence, failure, and branching are explicit in the type system instead of lurking at runtime.
The core D20Tek.Functional package provides the primitives - Optional<T>, Result<T>, Choice<T1, T2>, Identity<T>, and more - plus the composition operators (Map, Bind, Iter, Match) that let you chain them into readable pipelines. The companion D20Tek.Functional.AspNetCore package takes it to the edge of your app, turning a Result<T> into an IResult or ActionResult<T> without the usual boilerplate.
I played a lot with different functional styles and this library was the result of that experimentation. It is opinionated, but the opinions are based on what I have found to be the most practical and maintainable way to apply functional programming in C#. There are other libraries that provide similar funtionality, but this was my attempt to learn functional programming concepts. I have used this library and many of my own projects and continue expanding it as I run into now scenarios or cases.
Discriminated unions are coming to C# soon. That will change the capabilities of this library... hopefully some of it will go away. But some of the concepts, like Optional and Result will likely survive in my codebase (and with many other developers).
The API is modeled on F#'s functional core, so if you already work in F# the concepts and method names will feel familiar, and if you do not, you get a gentle on-ramp to the same ideas from within C#.
Why this matters in your day-to-day code
Functional programming favors expressions over statements, immutable data over mutable state, and explicit data flow over hidden side effects. That is not academic. It is the difference between bugs the compiler catches and bugs your users find.
Consider a typical lookup-and-transform. Here is the imperative version, with its silent failure modes:
public string GetDisplayName(int id)
{
var user = _repository.Find(id); // might return null
if (user == null)
{
return "(unknown)"; // easy to forget this branch
}
return user.Name.ToUpperInvariant();
}
And here is the same intent expressed as a pipeline, where the empty case cannot be forgotten because the type forces you to handle it:
public string GetDisplayName(int id) =>
_repository.Find(id) // returns Optional<User>
.Map(user => user.Name.ToUpperInvariant())
.DefaultValue("(unknown)");
The payoff shows up across the whole codebase:
- Fewer null reference bugs. Replacing
nullwith an explicitOptional<T>makes absence part of the type. The compiler and yourMatchhandlers force you to consider the empty case instead of discovering it in production. - Errors as values, not exceptions. With
Result<T>you model success and failure as data. Chain a sequence of operations withBindandMap, and any failure short-circuits the pipeline while carrying structuredErrorinformation forward. No exceptions for control flow, no losing context on the way up the stack. - Predictable, composable pipelines. Small, pure transformations combine into larger workflows. Operators like
Pipe,Fork, andAltlet you express intent directly instead of through nested conditionals and temporary variables. - Tests you actually want to write. Pure functions that take inputs and return outputs, without reaching into shared mutable state, are straightforward to test in isolation and need far less mocking.
- Effects where you can see them.
Iterand theUnittype isolate side effects such as logging and persistence at the edges of a pipeline, keeping the core logic pure and easy to follow.
What you get in the box
C# has picked up plenty of functional features over the years - LINQ, lambdas, pattern matching, records, and expression-bodied members - but it still lacks first-class discriminated unions and a standard option/result type. This library fills those gaps so you do not have to hand-roll them per project:
- Option monad -
Optional<T>provides a type-safe alternative tonull, withSome/None, safe accessors (DefaultValue,DefaultWith,Match), and conversions to and from nullable references. - Result monad -
Result<T>represents either a success value or an array of structuredErrorvalues, enabling exception-free error handling and railway-oriented flows. - Discriminated unions -
Choice<T1, T2>(and its async variants) model a value that is legitimately one of several types, matched exhaustively rather than with type checks. - Supporting monads and helpers -
Identity<T>lifts plain values into pipelines, theStateextensions support immutable state transitions,ValidationErrorsaccumulates multiple validation failures, andTryExceptturns try/catch/finally into expressions. - General combinators - extension methods such as
Pipe,Fork,Alt,IterateUntil,IfTrueOrElse, andForEachbring a functional style to any type. - LINQ query syntax -
Optional<T>andResult<T>implementSelectandSelectMany(andWhere), so you can compose them with thefrom ... select ...syntax you already know. - First-class async - the
D20Tek.Functional.Asyncnamespace mirrors the synchronous operators withMatchAsync,BindAsync,MapAsync, andIterAsync, plusTaskhelpers likeThenAsync, so asynchronous code composes just as cleanly. - ASP.NET Core integration -
D20Tek.Functional.AspNetCoreconvertsResult<T>into Minimal APIIResultor MVCActionResult<T>responses, mapsErrorvalues to RFC 7807 Problem Details, and provides filters that handle results automatically.
Enumerables are intentionally left to LINQ. Because IEnumerable<T> already follows a functional paradigm in C#, the library does not duplicate Map/Bind/Iter over collections to avoid confusion between similarly named methods.
Learn by reading real code
Documentation only goes so far. This project ships an extensive set of complete, runnable applications - not isolated snippets - so you can see how the types combine in practice. Clone the repo, open a sample, and step through it:
- Console applications (
samples/Apps) - TipCalc, GpaCalc, UnitConverter, GeneratePassword, a reusable TerminalAppTemplate, and the BudgetTracker and WealthTracker apps that show command-driven, result-based workflows. - Games (
samples/Games) - ChutesAndLadders and MartianTrail, which illustrate functional state transitions and pipeline-driven game logic. These were inspired by "Functional Programming in C#" by Simon J. Painter. - Blazor WebAssembly apps (
samples/Blazor) - BudgetTracker and WealthTracker, demonstrating functional patterns with async persistence in an interactive UI. - Web APIs (
samples/WebApi) - MemberService and TodoService, showing howD20Tek.Functional.AspNetCoreturnsResult<T>outcomes into clean HTTP responses and Problem Details.
Browsing these samples is the fastest way to see how the individual types combine into idiomatic, end-to-end functional C#.
Start here
- Read Getting Started to install the packages and write your first result-based workflow.
- Keep the API references open as you build:
- Core API Reference - the primary types in
D20Tek.Functional. - Async API Reference - the
D20Tek.Functional.Asyncnamespace. - ASP.NET Core API Reference - the
D20Tek.Functional.AspNetCorepackage.
- Core API Reference - the primary types in
- Pick the sample closest to what you are building and adapt it.
The next time a null slips through or an exception hijacks your control flow, you will have a better tool to apply to that job.
Install the Functionally package today!