D20Tek.Functional
A library that brings functional programming patterns to C#. Inspired by F#'s powerful
functional capabilities, this package provides types like Option, Result,
and Choice along with methods like Map, Bind, and Iter
to write more expressive, safer code.
Why Functional Programming in C#?
Functional programming emphasizes immutability, pure functions, and explicit handling of edge cases. While C# is primarily object-oriented, incorporating functional patterns can lead to:
- Fewer null reference exceptions - Use
Option<T>instead of null - Explicit error handling -
Result<T>makes success/failure obvious - More readable code - Chain operations with
MapandBind - Easier testing - Pure functions with no side effects
- Better maintainability - Predictable data flow through your application
Core Types
Option<T>
Represents a value that may or may not exist. Use Option instead of returning
null to make the possibility of missing values explicit in your API.
// Instead of returning null
public Option<User> FindUser(string id)
{
var user = _repository.GetById(id);
return user is not null
? Option<User>.Some(user)
: Option<User>.None();
}
// Consuming the Option
var greeting = FindUser("123")
.Map(user => $"Hello, {user.Name}!")
.DefaultValue("User not found");Result<T>
Represents an operation that can succeed with a value or fail with an error. Perfect for operations that might fail - no more throwing exceptions for expected failures.
public Result<Order> PlaceOrder(OrderRequest request)
{
if (!request.IsValid())
return Result<Order>.Failure("Invalid order request");
if (!_inventory.HasStock(request.Items))
return Result<Order>.Failure("Insufficient inventory");
var order = _orderService.Create(request);
return Result<Order>.Success(order);
}
// Chain operations, handling errors gracefully
var result = PlaceOrder(request)
.Bind(order => ProcessPayment(order))
.Map(order => SendConfirmation(order));Choice<T1, T2>
Represents a value that can be one of two types. Useful for operations that can return different types of results.
public Choice<CachedData, FreshData> GetData(string key)
{
var cached = _cache.Get(key);
if (cached is not null)
return Choice<CachedData, FreshData>.Choice1(cached);
var fresh = _repository.Fetch(key);
return Choice<CachedData, FreshData>.Choice2(fresh);
}Key Methods
| Method | Description |
|---|---|
Map |
Transform the inner value if present/successful |
Bind |
Chain operations that return Option/Result (flatMap) |
Iter |
Execute an action on the value if present (for side effects) |
Match |
Handle both cases (Some/None, Success/Failure) explicitly |
DefaultValue |
Provide a fallback value if None/Failure |
DefaultWith |
Provide a fallback using a factory function |
Installation
Install the NuGet packages using the Package Manager Console or .NET CLI:
# Core functional types dotnet add package D20Tek.Functional # ASP.NET Core integration (optional) dotnet add package D20Tek.Functional.AspNetCore
ASP.NET Core Integration
The D20Tek.Functional.AspNetCore package provides utilities for working with
Result<T> in WebAPI projects. It transforms results into appropriate
HTTP responses automatically:
- Success results return 200 OK with the value
- Failure results return appropriate error codes with ProblemDetails
- Eliminates repetitive error-handling boilerplate
app.MapGet("/api/orders/{id}", (string id, IOrderService service) =>
service.GetOrder(id)
.ToApiResult()); // Automatically converts Result<T> to IResult
app.MapPost("/api/orders", (OrderRequest request, IOrderService service) =>
service.PlaceOrder(request)
.ToCreatedApiResult(order => $"/api/orders/{order.Id}"));Railway-Oriented Programming
One of the key benefits of using Result<T> is enabling railway-oriented
programming - a pattern where your code follows a "happy path" while errors are automatically
shunted to a separate track.
public Result<OrderConfirmation> ProcessOrder(OrderRequest request)
{
return ValidateRequest(request) // Result<ValidatedOrder>
.Bind(CheckInventory) // Result<InventoryReserved>
.Bind(ProcessPayment) // Result<PaymentConfirmed>
.Bind(CreateOrder) // Result<Order>
.Map(SendConfirmationEmail); // Result<OrderConfirmation>
}
// If any step fails, subsequent steps are skipped
// The error propagates to the final resultSample Projects
The repository includes several sample console applications and games demonstrating functional programming patterns:
- ChutesAndLadders - Classic board game implemented functionally
- MartianTrail - Space adventure game using functional patterns
- Console Samples - Various examples showing Option, Result, and Choice usage
These samples are inspired by "Functional Programming in C#" by Simon J. Painter and demonstrate practical applications of functional concepts.
Why Not Just Use LINQ?
C# already has functional capabilities through LINQ for collections. This library complements LINQ by providing functional types for single values and operation results rather than sequences.
Use LINQ for working with collections. Use D20Tek.Functional for:
- Representing optional values (
Option<T>) - Explicit error handling (
Result<T>) - Discriminated unions (
Choice<T1, T2>)
Ready to write safer, more expressive C# code?