Fluent API and builders: language-level support #7325
Replies: 4 comments 5 replies
-
Looking at your "Person" example, it seems like the language already provides a very clean and easy to use system here with "records". You already get a terse way of declaring things, and you get the |
Beta Was this translation helpful? Give feedback.
-
Hi, I like the idea to clean this up. The only thing that I would change is the return type for a method, it’s now a bit unclear that a method explicitly returns this. What if you do it like this: public this WithHeader(string this.header); it would be more clear that the method returns an explicit this? |
Beta Was this translation helpful? Give feedback.
-
@amal-stack Today I also came up with an idea on how to add convenient and performance-effective support for the Fluent API. It is a slightly different approach that came to my mind. Read my idea, it is somewhat similar to yours. Performance-effective Fluent API support |
Beta Was this translation helpful? Give feedback.
-
Hey @amal-stack and @AlexRadch, Thank you very much for your insightful design work on fluent builders. I particularly like the 'with' and 'on' syntax. I agree that supporting fluent builders at the language level would be highly beneficial. You might be interested in my library M31.FluentAPI. It leverages source generation to create stepwise fluent builders for classes based on attributes. For example, the HttpRequest you discussed can be realized as follows: [FluentApi]
public class HttpRequest
{
[FluentMember(0)]
public HttpMethod Method { get; private set; }
[FluentMember(1)]
public string Url { get; private set; }
[FluentCollection(2, "Header")]
public List<(string, string)> Headers { get; private set; }
[FluentMember(3)]
public HttpContent Content { get; private set; }
[FluentMethod(3)]
public void WithJsonContent<T>(T body, Action<JsonSerializerOptions>? configureSerializer = null)
{
JsonSerializerOptions options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
configureSerializer?.Invoke(options);
Content = new StringContent(JsonSerializer.Serialize(body));
}
[FluentMethod(4)]
[FluentReturn]
public HttpRequestMessage GetMessage()
{
HttpRequestMessage request = new HttpRequestMessage(Method, Url);
request.Content = Content;
Headers.ForEach(h => request.Headers.Add(h.Item1, h.Item2));
return request;
}
} Usage: HttpRequestMessage message = CreateHttpRequest
.WithMethod(HttpMethod.Post)
.WithUrl("https://example.com")
.WithHeaders(("Accept", "application/json"), ("Authorization", "Bearer x"))
.WithJsonContent(
new { Name = "X", Quantity = 10 },
opt => opt.PropertyNameCaseInsensitive = true)
.GetMessage();
Console.WriteLine(JsonSerializer.Serialize(message)); Looking forward to any feedback you might have. Happy Coding!, |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Motivation
The fluent API and builder patterns are one of the most commonly used design patterns in C#. It is elegant, expressive and makes the code read like prose. Being used extensively and ubiquitously in libraries and public APIs, it serves as a fundamental building block (pun intended) of the language.
However, creating and extending fluent APIs may involve a lot of ceremony like:
this
Therefore, there have been several discussions to make life a bit simple by suggesting to introduce language-level support for creating fluent APIs and builders, just like how the
foreach
construct offers language-level syntactical support for the iterator design pattern. Here are a few of these discussions:#902 #311 #252 #7231 #169
The issues linked above suggest constructs like an implicit
this
return type to eliminate boilerplate.Objectives
The objectives of this feature include:
this
.Feature Description
The
fluent
modifierThe first step of creating a builder or a fluent interface is to create the type. A marker keyword
fluent
can be used to declare that the type backs a fluent interface:Behaviour of
fluent
typesDecaring a type with the
fluent
keyword permits enhanced syntax for creating chainable methods:WithName()
method which implicitly causes the method to return the enclosing typeBuilder
.string this.name
causes it to implicitly assign the method's parametername
to a private field of the classname
which would be available throughout the type's scope. Calling this method will set the autogenerated private field to the argument supplied during the call. Since this is what most methods do in builders, this can be implicit and the body can be omitted in this case.this
because the return type was omitted and the class is markedfluent
.Optional Body
The method can also have an optional body (block or expression-bodied), but the
return this
would be implicit, see theWithHeader()
method below (expression-bodied). Since the parametersstring name
andstring value
below do not have athis.
prefix, no implicit private field would be created for them:Apart from the chainable fluent methods, a
fluent
type can have regular methods, properties, constructors and other members but if any of the methods omit the return type, it would implicitly returnthis
.Example: Before and after
Builders with methods that merely set a private field and return
this
, or as I like to call them, "anemic builders", would be greatly simplified. Here's a simpleHttpRequestBuilder
before and after applying the enhancements discussed.Before
After
Usage
The client would be able to use the
HttpRequestBuilder
the same way before and after the enhancements:Improving the Stepwise Builder
A stepwise builder is where you want control over the order in which the methods are chained. This is especially useful when one step is a prerequisite for the next step.
Traditional approach
The traditional approach to creating a stepwise builder is to create an interface for each step. For instance:
Enhancement: the
on
clauseUsing the enhanced syntax, the interfaces can be completely eliminated using an
on
clause which will be permitted in types marked with thefluent
modifier which specifies the methods after which the current method can be called:Using the contextual
on
keyword, theon
clause specifies the methods after which the current method can be called. In the above example, the methodHasBodyTitle()
can only be called on the type thatHasHeader()
returns.It can be read as
HasBodyTitle
can be called onHasHeader
.Similarly, the
HasBodyDescription()
method can be called either after callingHasBodyTitle()
or directly after callingHasHeader()
allowing the caller to skip theHasBodyTitle()
step:Special case of the
on
clauseHere the
HasHeader()
method does not have anon
clause. This can also be implemented in a way where theHasHeader()
method is always available to be called either first or after every other method. To make theHasHeader()
method only available to be called after creating the object and not after calling any other methods (in other words, it always has to be the first method to be called), a special case of theon
clause can be introduced using one of the variations below:This would make
HasHeader()
unavailable after other methods are called. Methods with this specialon class
clause must be called on the object before other methods are called, even before methods without anon
clause. There can be several methods with this constraint, for example, to provide multiple chain paths.Alternative: the
with
clauseIf specifying after which other methods a particular method can be called using the
on
clause seems counter-intuitive, a possible alternative is to specify on a method the methods that can be further chained to it. Using the same example above:This can be read as
HasHeader()
can be continued withHasBodyTitle()
andHasBodyDescription()
.Here, it is unclear what method can be called first after creating the builder. So, it can be specified using the
from
keyword:This is equivalent to the special case of the
on
clause.A method without an
on
orwith
clause will always be available to be called after any method or first. But if there are methods with the special case of theon
clause (from
clause in the second alternative), only they can be called first.Fluent Interface with Inheritance
A common issue when creating a fluent API is extension via inheritance. When a derived builder class inherits from a base builder class, and when the consumer of the builder uses an object of the derived builder and calls a method of the base class, the methods of the derived class are masked out due to the base class method returning the type of the base class. A common but not so clean solution is to use recursive generics which come with their own limitations and also introduce unchecked casts with
this
.Traditional approach: recursive generics
Extending fluent interfaces with inheritance often requires the use of a recursive generic type parameter on the base class. This allows propagate derived class information up the base class because the base class returns the self-generic parameter for its fluent methods which are later closed by a derived class via inheritance. Closing the self-referencing generic disallows further extension. However, if the derived class leaves the self-type parameter for further extension, it cannot be instantiated.
Consider the following example:
Now
CircleBuilder
cannot be extended because the self-referencing generic parameterTBuilder
is closed. However, if it is left opened as so:In this case, it is open for extension but I cannot create an instance using
new CircleBuilder<TBuilder>()
because it is not possible to close theTBuilder
argument.Solution
When the types in the hierarchy are marked with a
fluent
modifier:The compiler can add self-referencing constraints implicitly or generate intermediate types so that the
WithName()
method is callable afterWithRadius()
. Since these methods do not explicitly specify a return type, the compiler may substitute the derived types on the base methods when the derived object is created or a source generator could be used to pull the base class methods down to the derived class.Benefits
HttpRequestBuilder
example above, the code has less boilerplate. Exactly 20 lines of code less after the enhancements, to be precise! Even though this example only has 7-8 methods. Considering the extent and ubiquity of builders and fluent types, this would have a huge impact. No explicit private field declarations, no return types in method signatures, noreturn this
and allows the method body to be optional.fluent
modifier well communicates the intent of the type that its methods are chainable and even with fewer lines of code, it is easy to comprehend.return this
is implicit.on
/with
clause helps to completely do away with interfaces for each step. Also, it is easier to visualize the order of the steps from the code than with interfaces.this
can be avoided. And calling base class methods does not hide derived class methods.Drawbacks
Beta Was this translation helpful? Give feedback.
All reactions