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 Map and Bind
  • 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 result

Sample 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?

An unhandled error has occurred. Reload 🗙