The popularity of immutable objects has made the Builder pattern crucial in C#. The most frequent use of builders in C# today is to simplify the creation of immutable objects using mutable objects, named builders, used solely to create the immutable ones. A prime example is the System.Collections.Immutable namespace, where each collection type has its own mutable builder.
Without builders, constructing complex immutable objects can be cumbersome because all properties must be set at once. The Builder pattern allows you to split the object creation process into several steps.
Typically, a builder type would almost be a copy of the corresponding immutable type, with mutable properties instead of read-only ones. An exception to this rule is for properties of collection type. Typically, the immutable type would have properties of other immutable types, while the builder will have properties of mutable types.
A proper implementation of the Builder pattern should exhibit the following features:
- A
Builderconstructor accepting all required properties. - A writable property in the
Buildertype corresponding to each property of the build type. For properties returning an immutable collection, the property of theBuildertype should be read-only but return the corresponding mutable collection type. - A
Builder.Buildmethod returning the built immutable object. - The ability to call an optional
Validatemethod when an object is built. - In the source type, a
ToBuildermethod returning aBuilderinitialized with the current values.
To support these features, some implementation details are necessary, such as internal constructors for the source and the Builder type.
As you can imagine, implementing the Builder pattern by hand is a tedious and repetitive task. Fortunately, precisely because it is repetitive, it can be automated using a Metalama aspect.
Alternative
An alternative to the Builder pattern in C# is the use of record types, init properties, and the with keyword. Using vanilla C# is always better than using an aspect if it covers your needs.
However, using records has some limitations:
- If an object must be built in several steps, several instances of a
recordare required (modified in chain with thewithkeyword), while a singleBuilderinstance would be necessary in this case. - Handling collections is more convenient with the Builder pattern.
- It is not possible to validate a
recordbefore it is made accessible.
In this series
In this series, we explain, step by step, how to implement the Builder pattern:
| Article | Description |
|---|---|
| Getting started | Presents a first working version of the GenerateBuilder aspect. |
| Handling derived classes | Discusses how to handle non-sealed types. |
| Handling immutable collections | Discusses how to generate mutable properties in the Builder class from immutable properties in the source class. |