Creating a Fluent API in C#

By FoxLearn 12/26/2024 7:29:22 AM   46
Fluent interfaces provide a smooth, discoverable way for developers to interact with functionality.

If you've ever used a method like StringBuilder.Append() in C#, then you've already experienced a fluent interface. Not only are fluent interfaces user-friendly, but they can also be simple to create with the right knowledge.

If you’re unfamiliar with what a fluent interface is, let me illustrate it with a simple example. This one calculates the total cost of a product after applying a series of discounts:

var totalCost = new Product(price: 100)
                 .ApplyDiscount(10)  // Apply 10% discount
                 .ApplyTax(5)         // Apply 5% tax
                 .FinalPrice();

Compare this fluent approach with a more traditional C# pattern using getters and setters:

var product = new Product(price: 100);
product.Discount = 10;
product.Tax = 5;

var totalCost = product.FinalPrice();

Both methods are valid, but why would you choose the fluent interface approach over the traditional one?

In my opinion, fluent interfaces make the process clearer and more structured, guiding developers to follow the correct steps. With the traditional method, there’s nothing preventing you from calling FinalPrice() before applying any discounts or taxes, which could result in incorrect results or errors. A fluent interface, on the other hand, ensures that the necessary steps are followed in order.

Additionally, fluent interfaces provide better encapsulation. It’s harder for another developer to accidentally modify critical values within the object, compared to the traditional approach where direct property access is possible.

// Potential issue in traditional approach:
if (product.Discount = 50) // Oops, mistake!

Now, let’s break down the core elements of a fluent interface using our example.

1. Entry Point

new Product(price: 100) // Entry point

Every fluent interface begins with an entry point where the chaining process starts. In our example, the entry point is the Product constructor, but it could be any method, static method, or property that initializes the chaining.

2. Chaining Methods

.ApplyDiscount(10)
.ApplyTax(5)

The chaining methods are responsible for setting the state of the object. These methods often set properties, but the important part is that they return the current object (this), enabling the chaining of further method calls.

3. Executor

.FinalPrice(); // Executor

The executor method is the point where the chain concludes, and a result is returned. You can have multiple executors, depending on your needs. The executor method is where you exit the fluent interface chain and get your final output.

How to Build the Fluent Interface in C#?

This is the entry point. The Product constructor initializes the price, and it’s where the fluent interface begins.

public class Product
{
    private readonly decimal _price;

    public Product(decimal price)
    {
        _price = price;
    }
}

Next, we add the chaining methods to modify the product’s properties:

public class Product
{
    private decimal _price;
    private decimal _discount;
    private decimal _tax;

    public Product(decimal price)
    {
        _price = price;
    }

    public Product ApplyDiscount(decimal percentage)
    {
        _discount = percentage;
        return this;
    }

    public Product ApplyTax(decimal percentage)
    {
        _tax = percentage;
        return this;
    }
}

Here, each method returns the Product object (this), allowing you to chain multiple method calls.

Finally, let’s implement the executor to return the final price after applying the discount and tax:

public class Product
{
    public decimal Price { get; private set; }
    public decimal Discount { get; private set; }
    public decimal Tax { get; private set; }

    public Product(decimal price)
    {
        Price = price;
    }

    public Product ApplyDiscount(decimal percentage)
    {
        Discount = percentage;
        return this;
    }

    public Product ApplyTax(decimal percentage)
    {
        Tax = percentage;
        return this;
    }

    public decimal FinalPrice()
    {
        var discountedPrice = Price * (1 - Discount / 100);
        var taxedPrice = discountedPrice * (1 + Tax / 100);
        return taxedPrice;
    }
}

Notice that the executor method FinalPrice() computes the final price after applying the discount and tax, and returns the result. This allows the developer to use the fluent interface to guide them through the process without accidentally skipping any important steps.