Using functional programming can result in more reliable, less buggy software. Let’s see five more advanced functional programming concepts in practice in ASP.NET Core.
Functional programming is a paradigm that opposes one of the principles of object orientation: state change. Although it may seem a bit radical, this allows you to have code predictability, since you have control over all object changes that occur during the execution of a program and nothing happens “behind the scenes.”
In this post, we will explore five advanced concepts of functional programming through practical ASP.NET Core examples and evaluate the pros and cons of each approach.
Functional programming is a software development paradigm that emphasizes a function-based model, avoiding state changes and side effects.
Unlike object-oriented programming (OOP), which focuses on objects that have state and behavior, functional programming makes use of mathematical expressions. This means that functions are treated as “first-class citizens” and can be assigned to variables, passed as arguments and returned by other functions.
Some languages are purely functional, such as F#, Haskell, Clojure, Scala and Elixir. However, although C# is not purely functional, it is multi-paradigm, which means that it also supports functional concepts.
If you don’t already have a basic knowledge of Functional Programming, I recommend that you read this post: Functional Programming in C#—A Brief Consideration for a more in-depth introduction.
No, it is not necessary to always use a functional approach, and in C#, which is a multi-paradigm language, this may not even be possible. More important than that is to use functional programming whenever it is relevant.
For example, in C#, the use of functional concepts such as immutability and pure functions, can improve the readability, testability and predictability of the code. However, there are scenarios where an object-oriented approach makes more sense, such as in the modeling of complex domains, where specialized approaches such as domain-driven design (DDD) can be used.
In multi-paradigm languages such as C#, the ideal is to adopt a hybrid style, taking advantage of the benefits of functional programming to reduce side effects and avoid state changes whenever possible, but without forcing its use when the object-oriented approach is more natural and productive.
Let’s look at some of the main advanced topics of functional programming. Each topic will contain examples of how to implement it in the traditional way using OOP, and then we will demonstrate how to use functional programming to solve some of the side effects of OOP and the advantages that the functional approach can bring.
All examples covered in the post are available in this GitHub repository: Advanced Functional Programming.
Avoiding state change is one of the pillars of functional programming. The object-oriented approach allows you to change the value of an object’s properties practically anywhere in the code, which can be convenient, but it can also lead to bugs in the code since there is no native control for state change, so it can occur at any time and place.
The relationship between bugs and state change is directly connected because many bugs arise due to problems in state management. For example, imagine an order in an ecommerce system that can be in the status “Pending,” “Paid” or “Shipped.” If, by some error, it is marked as “Shipped” without having gone through “Paid,” the system may behave unexpectedly, and this can trigger a wave of new bugs.
Furthermore, changing the state of an object makes it difficult for the programmer to see how and when the state was changed, often forcing the developer to debug the code and check each value that is being manipulated during its execution.
Another issue with changing the state of objects is that it requires the programmer to keep a “mental map” of all possible transitions that occur in the code flow. For example, in an ecommerce shopping cart where products can be added, removed and dynamically discounted, keeping all the rules in mind while reading the code becomes a real challenge.
To avoid these and other problems, we should limit state changes whenever possible. There will certainly be situations where changing the state of an object is a prerequisite. In these cases, there is nothing to be done. State changes exist in OOP and can be used, as long as they are done with caution.
Now, let’s take a practical look at an approach that uses state changes and another that uses the functional approach and analyze each of them.
Note the code below:
public class MutableOrder
{
public Guid Id { get; private set; }
public string Status { get; private set; }
public MutableOrder()
{
Id = Guid.NewGuid();
Status = "Pending";
}
public void Approve()
{
if (Status != "Pending") throw new InvalidOperationException("Order cannot be approved.");
Status = "Approved";
}
}
public async Task ApproveOrder(MutableOrder order)
{
order.Approve();
await _dbContext.SaveChangesAsync();
}
🚨 Issue: This code has a common example of state change, where the MutableOrder
class has a method called Approve()
that directly changes the value of the Status
property. This is not a good choice because, besides this method, there are several other places where this property has been changed. It would be very difficult to track down all these places, and this could easily result in a bug.
To avoid the state change and its side effects we can rewrite the code as follows:
public record ImmutableOrder(Guid Id, string Status);
public async Task ApproveOrder(ImmutableOrder order)
{
var approvedOrder = Approve(order);
_dbContext.Update(approvedOrder);
await _dbContext.SaveChangesAsync();
}
public ImmutableOrder Approve(ImmutableOrder order) =>
order.Status == "Pending" ? order with { Status = "Approved" }
: throw new InvalidOperationException("Order cannot be approved.");
✅ In this example, a record is used that is natively immutable, meaning you cannot change its state. Instead, the Approve()
method now returns a new instance of the ImmutableOrder
record. Note that for this, the expression order with { Status = "Approved" }
is used, which is a C# feature called record composition that creates a new Order object with the same value as the order object but with the Status
field changed to "Approved"
.
This approach creates a new instance of a record, preserving the values of the fields that were not modified and replacing the Status
value. This is an example of immutability, where the state of the original object is not changed, but rather a new instance is created with new values.
In addition, we are using the EF Core Update()
method to update the status of the record in the database. Since we created a new instance (approvedOrder
), it is necessary to inform EF Core to execute the changes.
You might be wondering: But what if I wanted to use an immutable class instead of a record? In that case, you could do the following:
public class ImmutableClassOrder
{
public Guid Id { get; }
public string Status { get; }
public ImmutableClassOrder(Guid id, string status)
{
Id = id;
Status = status;
}
public ImmutableClassOrder WithStatus(string newStatus) => new ImmutableClassOrder(Id, newStatus);
}
public async Task ApproveOrder(ImmutableClassOrder order)
{
var approvedOrder = Approve(order);
_dbContext.Update(approvedOrder);
await _dbContext.SaveChangesAsync();
}
public ImmutableClassOrder Approve(ImmutableClassOrder order) =>
order.Status == "Pending" ? order.WithStatus("Approved")
: throw new InvalidOperationException("Order cannot be approved.");
Note that the ImmutableClassOrder
class has all its fields read-only, which makes it impossible for their values to be modified outside the class, thus making its state immutable.
Therefore, the only option available is to create a new instance of the class, which in this example is being created through the ApproveOrder(ImmutableClassOrder order)
method, so there can be no change in state under any circumstances.
Higher-order functions are functions that receive other functions as arguments and can also return functions. In functional programming, this is an essential concept because it allows code reuse through the composition of functions (functions that call other functions).
To implement higher-order functions in ASP.NET Core and C# in general, we can use delegates, lambda expressions and anonymous methods.
The example below demonstrates how to create a higher-order function that receives another function as an argument:
public decimal NoDiscount(decimal amount) => amount;
public decimal TenPercentDiscount(decimal amount) => amount * 0.90m;
public decimal FixedDiscount(decimal amount) => amount - 5m > 0 ? amount - 5m : 0m;
public decimal ApplyDiscount(Func<decimal, decimal> discountStrategy, decimal totalAmount)
{
return discountStrategy(totalAmount);
}
public void ProcessDiscount()
{
decimal orderTotal = 100m;
decimal totalWithNoDiscount = ApplyDiscount(NoDiscount, orderTotal);
decimal totalWithTenPercentDiscount = ApplyDiscount(TenPercentDiscount, orderTotal);
decimal totalWithFixedDiscount = ApplyDiscount(FixedDiscount, orderTotal);
Console.WriteLine($"Original Total: ${orderTotal}");
Console.WriteLine($"No Discount Applied: ${totalWithNoDiscount}");
Console.WriteLine($"10% Discount Applied: ${totalWithTenPercentDiscount}");
Console.WriteLine($"Fixed Discount of $5 Applied: ${totalWithFixedDiscount}");
}
Note that ApplyDiscount()
receives a function (Func<decimal, decimal> discountStrategy
) as a parameter and returns the resulting value. Thus, we can say that ApplyDiscount()
is a higher-order function since its argument (which in this case is a function) is treated as a first-class citizen, allowing it to be passed as a parameter and used dynamically.
A good example of a higher-order function is LINQ’s Select function, which takes a function as an argument, as demonstrated in the example below. Note that an operation is passed as an argument to the Select method.
var names = new[] { "Alice", "John", "Thomas" };
var nameLengths = names.Select(name => name.Length);
If you inspect the Select method, you can see that the expected argument is indeed a function:
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
. . .
}
For a function to be considered “pure,” it must follow two main rules:
A pure function can be a simple data transformation, such as calculating a discount:
public decimal CalculateTax(decimal amount, decimal taxRate)
{
return amount * (1 + taxRate / 100);
}
✅ Always returns the same value for the same inputs.
✅ Has no side effects.
Now let’s implement the same example, but in an impure way.
public decimal CalculateTax(decimal taxRate)
{
var totalAmount = _amount * (1 + taxRate / 100);
if (totalAmount > 100000)
{
_logger.LogInformation($"Total amount generated with significant value: {totalAmount}");
}
return totalAmount;
}
❌ It does not always return the same result, as it depends on the value of the global variable _amount
.
❌ It has side effects (if condition and log).
Functional programming emphasizes the importance of avoiding exceptions. Some of the main reasons why we should avoid exceptions when programming functionally are:
Here is a common example of exception handling in ASP.NET Core:
public Order ExceptionHandleOrder(string orderId)
{
try
{
var order = _dbContext.Orders.FirstOrDefault(o => o.Id == orderId);
if (order == null)
throw new Exception("Order not found");
if (order.Total <= 0)
throw new Exception("Invalid total amount");
if (order.Paid == false)
throw new Exception("Order not yet paid");
return order;
}
catch (Exception ex)
{
throw new Exception($"Error: {ex.Message}");
}
}
🚨 Issue: This code uses exceptions to control the flow, making the flow less reliable and making debugging, maintenance and testing more difficult.
Railway-oriented programming (ROP) is a functional pattern for handling errors without using exceptions, inspired by the concept of railways. 🚆
Instead of linear code where errors interrupt execution, ROP emphasizes modeling the program flow as two parallel tracks:
Using ROP, we can avoid nested try/catch statements and compose functions without the need to manually check for errors.
In addition, ROP uses monads (Result<T>
, Either<T>
) to represent errors as values instead of throwing exceptions.
So, let’s implement the same example, but now using ROP to avoid exceptions.
First, we create a generic structure Result<T>
that has a function for success and another for error.
public record Result<T>(T? Value, bool IsSuccess, string? ErrorMessage = null)
{
public static Result<T> Success(T value) => new(value, true);
public static Result<T> Fail(string error) => new(default, false, error);
}
Then we create another class that has a Bind
method. This method allows us to chain operations that may fail without needing an if or try-catch.
It receives a Result<T>
, which represents the result of an operation that may have succeeded or failed. If it is successful (IsSuccess
is true), it applies the func
function, which transforms T
into a Result<U>
. If it fails (IsSuccess
is false), it returns a Result<U>
containing the same error.
public static class ResultExtensions
{
public static Result<U> Bind<T, U>(this Result<T> result, Func<T, Result<U>> func) =>
result.IsSuccess ? func(result.Value!) : Result<U>.Fail(result.ErrorMessage!);
}
Now, just call these methods to execute the custom validation that dispenses any type of exception:
public Result<Order> HandleOrder(string orderId) =>
GetOrder(orderId)
.Bind(VerifyTotal)
.Bind(VerifyPaid);
public Result<Order> GetOrder(string id) =>
_dbContext.Orders.FirstOrDefault(o => o.Id == id) is { } order
? Result<Order>.Success(order)
: Result<Order>.Fail("Order not found");
public Result<Order> VerifyTotal(Order order) =>
order.Total > 0
? Result<Order>.Success(order)
: Result<Order>.Fail("Invalid total amount");
public Result<Order> VerifyPaid(Order order) =>
order.Paid
? Result<Order>.Success(order)
: Result<Order>.Fail("Order not yet paid");
So, if you had an API that calls the HandleOrder()
method passing a non-existent id as an argument, instead of an exception, the response would be the following:
{
"value": null,
"isSuccess": false,
"errorMessage": "Order not found"
}
Lazy evaluation is a concept that provides a strategy for evaluating expressions, where a value is computed only when needed. In simple terms, in lazy evaluation, you only exert the necessary effort when it is needed.
Unlike eager evaluation, where expressions are computed immediately, lazy evaluation postpones execution until the result is actually requested.
Using lazy evaluation allows execution on demand—that is, the value of an expression is evaluated only when it is used. It also avoids unnecessary calculations because, if a value is never used, it will never be calculated. In addition, it optimizes performance and memory usage, as it reduces the computational load and avoids unnecessary allocation of resources.
Let’s see a simple yet explanatory example of how to implement lazy evaluation in ASP.NET Core.
Lazy<T>
—Immediate StartupNote the example below, where lazy evaluation is not used:
public class ExpensiveService
{
public ExpensiveService()
{
Console.WriteLine("ExpensiveService has been initialized!");
}
public void DoWork()
{
Console.WriteLine("Running operation...");
}
}
public class ExecuteClass
{
private readonly ExpensiveService _service;
public ExecuteClass()
{
_service = new ExpensiveService();
}
public void Execute()
{
Console.WriteLine("Execute method called!");
_service.DoWork();
}
}
// Execution
public void NoLazy()
{
var obj = new ExecuteClass();
Console.WriteLine("Object created!");
obj.Execute();
}
If you call the NoLazy()
method through an API, for example, you will get the following output in the terminal:
ExpensiveService has been initialized!
Object created!
Execute method called!
Running operation...
🚨 Issue: The ExpensiveService
class was initialized before it was even used.
Lazy<T>
—On-Demand StartupNow, let’s repeat the previous example but using the Lazy approach:
public class ExpensiveService
{
public ExpensiveService()
{
Console.WriteLine("ExpensiveService has been initialized!");
}
public void DoWork()
{
Console.WriteLine("Running operation...");
}
}
public class ExecuteLazyClass
{
private readonly Lazy<ExpensiveService> _service;
public ExecuteLazyClass()
{
_service = new Lazy<ExpensiveService>(() => new ExpensiveService());
}
public void Execute()
{
Console.WriteLine("Execute method called!");
_service.Value.DoWork();
}
}
// Execution
public void UsingLazy()
{
var obj = new ExecuteLazyClass();
Console.WriteLine("Object created");
obj.Execute();
}
If you call the UsingLazy()
method, you get the following output in the terminal:
Object created
Execute method called!
ExpensiveService has been initialized!
Running operation...
✅ By using lazy evaluation, the ExpensiveService
class was only initialized when the Execute
method was called, saving resources if it was never used.
Functional programming is a paradigm that allows the creation of more reliable software because it follows some principles that slow down and, in many cases, prevent the generation of bugs.
To verify the efficiency of functional programming, in this post we covered five advanced topics: state change, higher-order functions, pure functions, exception handling and lazy evaluation. Additionally, we explored the advantages of each one in practice.
Although building functional code requires more concentration and capacity from the developer, the long-term benefits outweigh the initial effort. The predictability of the system’s behavior, the ease of testing and the reduction of side effects make software maintenance and evolution lighter and more efficient.
So, whenever possible, consider using functional programming in your code for more reliable software that is less prone to bugs. This will certainly bring significant benefits to you and everyone involved in the project.
Next up: Explore more ASP.NET Core posts.