Introduction
Functional programming (FP) is a paradigm where the main focus, as the name suggests, is on functions. The most important aspect of FP is immutability. Many of our bugs come from mutable state, meaning that running the same function multiple times with the same parameters can produce different results, making program behavior unpredictable.
I learned these concepts through the book "Learning Functional Programming: Managing Code Complexity by Thinking Functionally", written by Jack Widman and published by O'Reilly.

Book cover of Learning Functional Programming: Managing Code Complexity by Thinking Functionally.
Example of a Problem with Mutable State
A common example is how we normally handle errors, as shown in the C# example below:
public void SendOrder(Order order)
{
if(string.IsNullOrEmpty(order.Address))
AddError("Address cannot be empty.");
/// Method continuation...
}
If we run this method twice, SendOrder
may produce unexpected side effects. For instance, if
AddError
modifies some internal shared state of the service or maintains a global error list, calling
SendOrder
with different orders can “leak” information from one order to another. This is a classic
example of problems caused by mutable state.
Order order1 = new Order { Address = "" };
Order order2 = new Order { Address = "Lo Barnechea, 42" };
OrderService service = new OrderService();
service.SendOrder(order1);
service.SendOrder(order2);
Result Pattern
The Result Pattern is a way to make the outcome of an operation explicit, indicating whether it succeeded or failed and carrying additional information, such as error messages. This eliminates side effects and forces the consumer to handle errors clearly.
There are several libraries that provide this pattern, but to avoid external dependencies, I prefer to implement it manually.
public class Result
{
public bool IsSuccess { get; }
public string Error { get; }
public T Value { get; }
private Result(bool isSuccess, T value, string error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public static Result Ok(T value) => new Result(true, value, null);
public static Result Fail(string error) => new Result(false, default, error);
}
[MustUseReturnValue]
public Result ValidateOrder(Order order)
{
if (string.IsNullOrEmpty(order.Address))
return Result.Fail("Address cannot be empty.");
return Result.Ok(order);
}
The Result Pattern enforces error checking and avoids mutability in methods. The [MustUseReturnValue]
attribute from the JetBrains.Annotations
package helps ensure that the result is not ignored by the
developer using the method.
To integrate these validations with ASP.NET Core in your web layer, you can create an extension method and use it in your Controllers:
public static class ModelStateExtensions
{
public static void AddResultErrors(this ModelStateDictionary modelState, Result result, string key = null)
{
if (!result.IsSuccess)
{
modelState.AddModelError(key ?? string.Empty, result.Error);
}
}
}
[HttpPost]
public IActionResult CreateOrder(Order order)
{
var result = ValidateOrder(order);
ModelState.AddResultErrors(result, nameof(order));
if (!ModelState.IsValid)
return BadRequest(ModelState);
SendOrder(order);
return Ok();
}
Immutability with Records
In C#, records are reference types that are immutable by default, making them ideal for functional
programming. They allow creating immutable objects after instantiation, avoiding side effects. Another advantage is
the built-in with
operator to create object copies. I also like to think of them as a hybrid between a
class and a struct: they are reference types, but equality with the ==
operator is based on property
values rather than reference.
public record Order(string Address, int Quantity);
A good example is creating a copy of an order using the with
operator:
var order = new Order("Street A, 10", 2);
var orderCopy = order with { Address = "Street B, 20" };
This ensures each instance remains consistent, avoiding errors from modifying values by reference.
F#
If you liked what you’ve read so far, one way to practice FP is with F#, a functional language in the .NET ecosystem, fully compatible with existing C# code but focused on immutability, expressions, and pure functions. It encourages writing declarative code, where mutable state is avoided whenever possible, making programs more predictable and safe.
let address = "Street A, 10"
// address <- "Street B, 69" // This causes a compile-time error
// For mutability, we need to use the mutable keyword:
let mutable counter = 0
counter <- counter + 1
What I love most about F# are type unions. As of this article’s writing, C# still does not support type unions, which are a way to implement the Result Pattern, as shown in the F# example below:
type OrderResult =
| Success of Order
| Error of string
let validateOrder order =
if order.Address = "" then
Error "Address cannot be empty"
else
Success order
Conclusion
Remember that functional programming is just one of the paradigms available. It is not the solution to every problem, and you might confuse colleagues if you try to use it where object-oriented programming would clearly be better. As I always argue, it’s important to know a bit of everything and choose the right tool for each context. FP is particularly useful for multithreading, data transformations, or writing testable code. Developer preference also plays a role; I personally prefer working with method returns rather than state. The great advantage of C# and F# is that they are multiparadigm languages.