Functional C# Cookbook: Solutions for Real-World Problems
Introduction
Functional programming techniques can make C# code more predictable, testable, and maintainable. This cookbook collects practical recipes—small, focused examples you can apply immediately—to solve common real-world problems using functional C# patterns: immutability, pure functions, higher-order functions, composition, and safe error handling.
1. Immutable DTOs and Value Objects
Problem: Mutable data structures cause hard-to-find bugs when shared across threads or passed between layers.
Recipe:
- Use readonly auto-properties and init accessors.
- Prefer records for value-based equality.
- Enforce invariants in constructors or static factory methods.
Example:
public record Money(decimal Amount, string Currency);
public record OrderItem
{
public Guid Id { get; init; } = Guid.NewGuid();
public string Name { get; init; }
public Money Price { get; init; }
private OrderItem() { } // for deserialization
public OrderItem(string name, Money price)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Price = price;
}
}
When to use: DTOs passed between services, domain value objects, multi-threaded contexts.
2. Composition with Functions and Delegates
Problem: Large methods with multiple steps are hard to test and reuse.
Recipe:
- Break logic into small pure functions.
- Compose using Func or custom delegates.
- Use extension methods for pipeline-style composition.
Example:
Func<Order, Order> applyDiscount = order =>
order with { Total = order.Total 0.9m };
Func<Order, Order> addTax = order =>
order with { Total = order.Total 1.08m };
Func<Order, Order> pipeline = order => addTax(applyDiscount(order));
// or using extension:
public static T Pipe<T>(this T input, Func<T,T> fn) => fn(input);
var final = originalOrder .Pipe(applyDiscount)
.Pipe(addTax);
When to use: Transformation pipelines, ETL tasks, request processing chains.
3. Safe Error Handling with Result/Either Types
Problem: Exceptions for control flow and nulls lead to fragile, unclear code.
Recipe:
- Implement a Result type or use a library (e.g., LanguageExt, CSharpFunctionalExtensions).
- Return Result from operations, compose with Bind/Map.
Example (minimal Result):
public record Result<T, E>(bool IsSuccess, T? Value, E? Error);
public static Result<T2, E> Map<T, T2, E>(this Result<T, E> r, Func<T, T2> f) =>
r.IsSuccess ? new Result<T2, E>(true, f(r.Value!), default) : new Result<T2, E>(false, default, r.Error);
When to use: IO operations, validation chains, service orchestration.
4. Declarative LINQ and Immutable Collections for Data Processing
Problem: Imperative loops mutate state and are verbose.
Recipe:
- Use LINQ for transformations and filtering.
- Prefer ImmutableArray/ImmutableList from System.Collections.Immutable or use IEnumerable pipelines that materialize only when needed.
Example:
using System.Collections.Immutable;
ImmutableArray<OrderItem> expensive = items .Where(i => i.Price.Amount > 100m)
.OrderByDescending(i => i.Price.Amount)
.ToImmutableArray();
When to use: Reporting, batch processing, preparing data for UI.
5. Concurrency with Pure Functions and Actors
Problem: Shared mutable state causes race conditions in concurrent code.
Recipe:
- Keep functions pure; share only immutable data.
- For mutable state, confine it to an actor or use System.Threading.Channels, MailboxProcessor patterns, or libraries like Akka.NET.
Example (minimal actor with Channel):
var channel = Channel.CreateUnbounded<Func<Task>>();
async Task ActorLoop()
{
await foreach (var work in channel.Reader.ReadAllAsync())
await work();
}
// Post work:
await channel.Writer.WriteAsync(async () => { /* mutate isolated state */ });
When to use: Real-time systems, background workers, state machines.
6. Asynchronous Streams and Reactive Patterns
Problem: Combining real-time data with backpressure and composition is complex.
Recipe:
- Use IAsyncEnumerable for async streams.
- Apply LINQ-like operators (Where, Select) with await foreach.
- For richer operators, consider System.Reactive (Rx.NET).
Example:
async IAsyncEnumerable<int> GenerateAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100);
yield return i;
}
}
await foreach (var x in GenerateAsync().Where(x => x % 2 == 0))
Console.WriteLine(x);
When to use: Event processing, streaming APIs, UI updates.
7. Functional Testing: Property-Based and Pure Unit Tests
Problem: Tests that rely on mutable shared state are flaky.
Recipe:
- Write pure functions and test them deterministically.
- Use property-based testing (FsCheck) for broader coverage.
- Mock side-effects using small adapters.
Example:
- Test pure mapper functions with many inputs; use FsCheck for invariants.
When to use: Core business logic, transformers, validation.
8. Interop with OOP Codebases
Problem: Introducing FP to existing OOP systems needs gradual adoption.
Recipe:
- Start with small value objects, pure helpers, and Result-returning methods.
- Encapsulate side-effects at the edges (repositories, services).
- Use extension methods and adapter classes to bridge styles.
Example:
- Replace void methods with functions returning Result and refactor callers stepwise.
9. Performance Considerations
Problem: Overuse of allocations (e.g., LINQ, closures) can hurt throughput.
Recipe:
- Measure with BenchmarkDotNet.
- Prefer structs for hot-path small types (Span, Memory where appropriate).
- Use ValueTask for frequently awaited performance-critical async methods.
- Cache delegates when composing pipelines in hot paths.
When to use: High-performance servers, tight loops.
10. Recipes for Common Real-World Scenarios
- Validation pipeline: Compose small validators returning Result and short-circuit on first failure.
- Retry with backoff: Implement pure policy functions describing delays and an executor handling retries.
- Bulk import: Build a transform pipeline (IEnumerable -> Map -> Filter -> Batch -> Persist) using immutable collections.
- Feature flags: Use pure predicates plus configuration injected at startup; avoid scattered ifs.
Conclusion
Adopt functional C# incrementally: prefer immutability, small pure functions, and explicit error handling. Use the recipes above as practical patterns to reduce bugs and improve maintainability in real systems.
Further reading: explore records, System.Collections.Immutable, LanguageExt/CSharpFunctionalExtensions, Rx.NET, and FsCheck.