I am taking a stab at writing a fluent API in C#. What I am trying to do is make a builder that can set some properties. The properties can be set in any order, and all of them are optional. After a property is already set, it makes little sense to still show the method that sets it in intellisense, so I would like to hide it if it is already set. Here is an example of the type of API I would like with just 3 properties.
public interface ISoldier
{
string Name { get; set; }
string Rank { get; set; }
int SerialNumber { get; set; }
}
public interface ISoldierBuilder
{
IInterrogationStarter InterrogateSoldier();
}
public interface IInterrogationEnder
{
ISoldier FinishInterrogation();
}
// No properties set (3 that can still be set)
public interface IInterrogationStarter : IInterrogationEnder
{
IBuilderWithNameSet WithName(string value);
IBuilderWithRankSet WithRank(string value);
IBuilderWithSerialNumberSet WithSerialNumber(int value);
}
// One property set (2 that can still be set)
public interface IBuilderWithNameSet : IInterrogationEnder
{
IBuilderWithNameAndRankSet WithRank(string value);
IBuilderWithNameAndSerialNumberSet WithSerialNumber(int value);
}
public interface IBuilderWithRankSet : IInterrogationEnder
{
IBuilderWithNameAndRankSet WithName(string value);
IBuilderWithRankAndSerialNumberSet WithSerialNumber(int value);
}
public interface IBuilderWithSerialNumberSet : IInterrogationEnder
{
IBuilderWithNameAndSerialNumberSet WithName(string value);
IBuilderWithRankAndSerialNumberSet WithRank(string value);
}
// Two properties set (1 that can still be set)
public interface IBuilderWithNameAndRankSet : IInterrogationEnder
{
IInterrogationEnder WithSerialNumber(int value);
}
public interface IBuilderWithNameAndSerialNumberSet : IInterrogationEnder
{
IInterrogationEnder WithRank(string value);
}
public interface IBuilderWithRankAndSerialNumberSet : IInterrogationEnder
{
IInterrogationEnder WithName(string value);
}
I left off the implementation because it isn't required to demonstrate what I am trying to do. Now, if you actually copy this code into a project you can see what I am trying to do. Once a property is set, it is no longer available in the list in intellisense.
ISoldierBuilder builder;
ISoldier soldier1 = builder.InterrogateSoldier()
.WithName("Bob")
.WithRank("Corporal")
.FinishInterrogation();
ISoldier soldier2 = builder.InterrogateSoldier()
.WithSerialNumber(12345)
.FinishInterrogation();
ISoldier soldier3 = builder.InterrogateSoldier()
.WithRank("Captain")
.WithSerialNumber(55533)
.WithName("John")
.FinishInterrogation();
For example, with soldier1, once we call .WithName("Bob").WithRank("Corporal"), all that is left in the available choices are:
WithSerialNumber
FinishInterrogation
Setting the name again would just be confusing - which one is the actual name we need to put into the ISoldier instance?
Now imagine trying to expand this simple example to 10 properties. My question is - how can I make my optional properties disappear from intellisense after they are set without creating a huge number of interfaces that grow exponentially with the number of properties?
More Context
The goals of the fluent API are to make it easier to write, easier to read, and to enforce business rules. If we haven't accomplished those, then there is little point in creating a fluent API.
The Long Story
In my research, I discovered that the definition of a fluent interface is one where the words flow together into a sentence and can be spoken aloud. However, in practice true fluent APIs are quite rare because of the amount of extra work they require to implement. A typical compromise is to make one that is easily readable, but doesn't necessarily make sense to speak aloud.
I would like to go a little bit beyond that because in my own experience with existing fluent API implementations - I find that they are usually difficult to use and require quite a bit of research to learn how to configure. So I am adding an additional constraint that the API must be as easy to work with - preferably by discovering the available options using intellisense.
Also, I discovered that one of the most compelling reasons to use a fluent API is the ability to enforce some business rules at the compiler level. I read some great articles by Peter Vogel that go into some details about how to enforce business rules and used some of these techniques to enforce 13 of the business rules I need for my application in a way that won't compile unless the rules are followed. This is the best argument yet for using a fluent API, as just making an API that is readable is nothing more than syntactic sugar, but ensuring business rules are enforced is a much more practical and functional use.
Furthermore, "readability" is subjective and depends on who you anticipate to read the API. I am willing to make a few compromises here because the intended audience are developers.
The back story is that I have already implemented a non-functional prototype that enforces several of the business rules I have, as well as changes properties depending on their context because they are used differently when the context changes. I am hoping there is some way to use generics, attributes, or some other compiler magic to solve this one extra business rule without having to make dozens of extra interfaces to support it, and then have to change all of those interfaces when a new property is added.
The fluent API is also going to be an alternative to XML for semantically marking up a hierarchy of objects, which is yet another compelling argument for its usage. Some of the business rules (which haven't yet been implemented) deal with property inheritance from parent objects to their descendant objects.