The idea of fluent interfaces is not new, and has many forms. The basic idea is to use a single statement to encompass a series of operations that are natural, or at least simple, to use. There are very few fluent interfaces in .NET – the most well known, which has some fluent interface semantics, is the System.Text.StringBuilder class. Here’s a simple example:
StringBuilder is a simple example, let’s try to create something of our own. Here’s a simple Customer class with a fluent interface:
Properties are not suited for a fluent interface because property setters cannot return anything, so chaining is impossible. This means we have to turn properties into methods for “fluency”. With the above example, we can write code like the following (Item is a some class with a constructor accepting a string):
This is all good and well. Now let’s add a Customer-derived type like so:
Now let’s try to create a CorporateCustomer and initialize with the fluent interface:
Unfortunately, the code fails to compile. Can you spot the problem?
The problem is that WithAccountManager returns a Customer and not a CorporateCustomer, so the compiler rejects the call to WithPremierSupport. How can we solve that?
We can try to make the WithAccountManager method generic, like so:
In this case, the customer-creating code still does not compile. Can you spot the problem now, dear reader?
The problem is more subtle; in fact, the code that creates a plain Customer fails to compile as well. The problem is that the compiler cannot infer the generic argument automatically, because the input arguments to WithAccountManager have nothing to do with customer, and the return type can be anything that derives from customer – the compiler can’t simply select one – it does not understand the intent of the method – returning this as the “real” type of the object.
How can we solve that? One obvious way is to specify the generic arguments like so:
This works, but using it is very tedious. We just want to get rid of the explicit generic argument and still return the correct type. The solution? Extension methods:
With this in place (and the original WithAccountManager method removed), we can simply write:
This works because the compiler can deduce the correct type because of the first argument to the extension method; and this will continue to work correctly even if new classes deriving from Customer or CorporateCustomer are created.
Don’t you just love extension methods?