Where C# is the most dominant language in the .NET world, other languages are built on top of the Framework that deserves their respective place. F# is strong when it comes down to functional programming! In this blog post, we will leverage the power of F# and C# to showcase where both excel!
Why does it work?
Before I go into the details of the code, I want to explain why this works. Why can we use F# code in C# and vice versa? The answer is simple: the .NET Framework is a runtime that can execute code written in different languages. The runtime is responsible for compiling the code to an intermediate language (IL) that the runtime can execute. If your code is dependent on other libraries, you are not relying on the source code but the IL code of that library. I already had a whole blog post about that: "What is the difference between C#, .NET, IL and JIT?". Have a read here if you are interested in more details.
F#
Here is a very brief introduction: F# is a functional-first programming language in the .NET ecosystem. It is designed to provide the expressive power of functional programming alongside the robustness of object-oriented paradigms. It enjoys all of the benefits of the .NET Framework itself: open-source, platform-independent, and strongly typed.
let rec factorial n =
if n = 0 then 1 else n * factorial (n - 1)
printfn "Factorial of 5 is %d" (factorial 5)
F# also has some distinct features C# does not offer, the most prominent: Discriminated unions.
The use case
I want to showcase where F# excels - and oftentimes, that can be the domain layer of your application. Especially then when you have a lot of calculations (basically where functional programming excels in the first place). Let's create a small domain project that is written in F# and see how it could look like in C#. I will create a "small online" shop that can take orders and calculate a discount (a percentage or a fixed amount). Before we go into F#, have a look at the C# code:
public class Product
{
public string Name { get; set; }
public decimal Price { get; set; }
}
public class OrderLine
{
public Product Product { get; set; }
public int Quantity { get; set; }
}
public enum DiscountType { Percentage, FixedAmount }
public class Discount
{
public DiscountType Type { get; set; }
public decimal Value { get; set; }
}
public record Order(List<OrderLine> OrderLines, Discount Discount)
{
public decimal CalculateTotalPrice()
{
decimal subtotal = OrderLines.Sum(ol => ol.Product.Price * ol.Quantity);
decimal totalDiscount = 0M;
if (Discount != null) {
totalDiscount = Discount.Type == DiscountType.Percentage
? subtotal * Discount.Value / 100M
: Discount.Value;
}
return subtotal - totalDiscount;
}
}
Now for the sake of simplicity, I left out a lot of validation and error handling. I hope you get the idea anyway. Now let's see how this would look like in F#:
type Product = { Name: string; Price: decimal }
type OrderLine = { Product: Product; Quantity: int }
type Discount =
| Percentage of float
| FixedAmount of decimal
type Order(orderLines: OrderLine list, discount: Discount option) =
member this.CalculateTotalPrice() =
let subtotal = List.sumBy (fun ol -> ol.Product.Price * (decimal ol.Quantity)) orderLines
let totalDiscount =
discount
|> Option.map (fun d ->
match d with
| Percentage p -> subtotal * (decimal p / 100M)
| FixedAmount f -> f)
|> Option.defaultValue 0M
subtotal - totalDiscount
Wow - that is really slim. Let's go through the code step by step. First, we have the Product
and OrderLine
types. They are just simple records that hold the data. Then we have the Discount
type. This is a discriminated union that can either be a Percentage
or a FixedAmount
. The Order
type class takes a list of OrderLine
and an optional Discount
. The CalculateTotalPrice
method is the same as in the C# example. The only difference is that we use the Option
type to handle the null
case. The Option
type is a discriminated union that can either be Some
or None
. We can use pattern matching to handle both cases. In this case we use the Option.map
function to map the Discount
to a decimal
value. If the Discount
is None
we use the Option.defaultValue
function to return 0M
.
Now we can easily integrate that to a controller:
[ApiController]
[Route("api/orders")]
public class OrderController : ControllerBase
{
private static readonly List<Order> Orders = new();
[HttpPost]
public ActionResult<Order> CreateOrder([FromBody] List<OrderLine> orderLines, [FromBody] Discount discount)
{
var fsharpOrderLines = ListModule.OfSeq(orderLines);
var order = new Order(fsharpOrderLines, discount);
Orders.Add(order);
return CreatedAtAction(nameof(GetOrderById), new { id = Orders.Count - 1 }, order);
}
[HttpGet("{id:int}")]
public ActionResult<Order> GetOrderById(int id)
{
if (id < 0 || id >= Orders.Count)
{
return NotFound();
}
return Orders[id];
}
[HttpGet("{id:int}/total")]
public ActionResult<decimal> GetOrderTotalPrice(int id)
{
if (id < 0 || id >= Orders.Count)
{
return NotFound();
}
return Orders[id].CalculateTotalPrice();
}
[HttpGet]
public ActionResult<IEnumerable<Order>> GetAllOrders()
{
return Orders;
}
}
There might be a very special line here that needs some clarification:
var fsharpOrderLines = ListModule.OfSeq(orderLines);
The ListModule.OfSeq
function is a helper function that converts a List<T>
to a seq<T>
. The seq<T>
type is a sequence of elements. It is a lazy collection that is only evaluated when needed. This is a very powerful concept that is used a lot in F#. You can read more about it here. In this case we need to convert the List<T>
to a seq<T>
because the Order
type expects a seq<T>
as input.
Conclusion
I hope this blog post gave you a good overview of how you can use F# and C# together. It can be a very powerful tool, and F# has some unique strengths that can make your life easier!