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
Builder
constructor accepting all required properties. - A writable property in the
Builder
type corresponding to each property of the build type. For properties returning an immutable collection, the property of theBuilder
type should be read-only but return the corresponding mutable collection type. - A
Builder.Build
method returning the built immutable object. - The ability to call an optional
Validate
method when an object is built. - In the source type, a
ToBuilder
method returning aBuilder
initialized 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
record
are required (modified in chain with thewith
keyword), while a singleBuilder
instance would be necessary in this case. - Handling collections is more convenient with the Builder pattern.
- It is not possible to validate a
record
before 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. |