The Builder Pattern in C# [2024]

by Metalama Team on 21 Oct 2024

The popularity of immutable objects has made the Builder pattern one of the most essential in C#. In this article, we’ll discuss which use cases are a good fit for the Builder pattern and how to practically implement it in .NET, with real-world examples and source code on GitHub. We’ll see different tools to automatically generate the implementation instead of writing it by hand.

What is the Builder pattern?

The Builder design pattern is a creational design pattern that aims to separate the creation process of a complex object from the object itself.

Instead of creating an object directly using its constructor, you use a Builder class. When you’re done configuring the object, you call a Build() or ToImmutable() method of the Builder class, which returns the constructed object.

When to use a Builder Pattern?

There are basically 3 uses cases for the Builder pattern:

  1. Complex creation of immutable objects
  2. Abstracting the instantiation process
  3. Async creation of objects

Use case 1: Complex creation of immutable objects

The most common use case for the Builder pattern is when you want to use a mutable object during the initialization phase of an object but want the constructed object to be immutable.

To take a real-word example, suppose you’re building a clone of Amazon and want to start selling books. You have an immutable Book class. To make it possible to edit a book, you create a BookBuilder object with mutable properties. In the Build method, you check that all properties have acceptable values before instantiating the immutable object.

public record Book( string Title, ImmutableArray<string> Authors, ImmutableArray<string> Tags );

public class BookBuilder
{
    public string? Title { get; set; }

    public List<string> Authors { get; set; } = new();

    public List<string> Tags { get; set; } = new();

    public virtual Book Build()
    {
        return new Book( this.Title!, [..this.Authors], [..this.Tags] );
    }
}

{}

The full source code of examples in this article is available on .

You might want to create a Builder class for your immutable objects in the following scenarios:

  1. When building an interactive UI, you must bind the UI controls to writable properties.
  2. When you want to allow for initialization in multiple steps.

    For instance, imagine that the Build method depends on a service that tags the books right before an immutable instance is created:

    public class TaggingService
     {
         private readonly List<Action<BookBuilder>> _taggers = new();
        
         public void RegisterTagger( Action<BookBuilder> tagger ) => this._taggers.Add( tagger );
        
         public void Tag( BookBuilder bookBuilder )
         {
             foreach ( var tagger in this._taggers )
             {
                 tagger.Invoke( bookBuilder );
             }
         }
     }
    
  3. When you must validate the object before creating it, and you cannot do it before knowing the value of all properties.

    public class ValidatingBookBuilder : BookBuilder
     {
         public override Book Build()
         {
             if ( this.Title == null )
             {
                 throw new ValidationException( "The Title property must not be null." );
             }
        
             if ( this.Authors.Count == 0 )
             {
                 throw new ValidationException( "There must be at least one author." );
             }
        
             if ( this.Tags.Contains( "Kid" ) && this.Tags.Contains( "Adult" ) )
             {
                 throw new ValidationException( "The 'Kids' and 'Adults' tags are exclusive." );
             }
        
             return base.Build();
         }
     }
    
  4. When the optimal data structure of the immutable object differs from the optimal data structure of the builder. A typical example is the System.Collections.Immutable namespace, where immutable collections and their builders have radically different implementations.

  5. When you want to ensure that your callers only modify the objects when it is allowed. A good example of this use case is the .NET Core AppBuilder pattern. When creating an API to build components, you want to allow API users to modify the configuration during the initialization phase by offering an API like AddMyComponent(Action<ComponentMyBuilder>? builder = null). However, you want to forbid them from modifying the configuration after the component has started.

Use case 2: Abstraction of the instantiation process

In the “Gang of Four’s” seminal Design Patterns book, the Builder pattern is described as a way to abstract the process of building objects.

Suppose you want to build an object of type IBook. The IBook interface has two implementations, PaperBook and EBook, which differ in their implementation of the Deliver method. For some reason, you want the implementation classes to remain internal. To make it possible to create IBook instances, you provide a new public interface: IBookBuilder. This interface is meant to be bound to the UI, so its properties are not strongly validated: we only ask the object to be valid when the Build method is called. The IBookBuilderFactory interface supplies IBookBuilder for the required kind of book. To get an IBookBuilderFactory, the caller will use IServiceProvider.

This long chain of invocation (IBookBuilderFactoryIBookBuilderIBook) can seem cumbersome, but it’s a realistic design if you must pull a dependency, for instance IIdGenerator, from IServiceProvider.

Here is the public API:

public interface IBook
{
    string Title { get; }

    ImmutableArray<string> Authors { get; }

    void Deliver();
}

public interface IBookBuilder
{
    string? Title { get; set; }

    List<string> Authors { get; }

    IBook Build();
}

public enum BookKind
{
    Paper = 1,
    EBook = 2
}

public interface IBookFactory
{
    IBookBuilder CreateBookBuilder( BookKind bookKind );
}

This API is meant to be used as follows:

var factory = serviceProvider.GetRequiredService<IBookFactory>();
var builder = factory.CreateBookBuilder( BookKind.Paper );
builder.Title = "Dix contes de Perrault";
builder.Authors.Add( "Charles Perrault" );
var book = builder.Build();
book.Deliver();

Let’s look at the implementation of immutable classes:

internal abstract record Book( int Id, string Title, ImmutableArray<string> Authors ) : IBook
{
    public abstract void Deliver();
}

internal record PaperBook( int Id, string Title, ImmutableArray<string> Authors )
    : Book( Id, Title, Authors )
{
    public override void Deliver() => throw new NotImplementedException();
}

internal record EBook( int Id, string Title, ImmutableArray<string> Authors )
    : Book( Id, Title, Authors )
{
    public override void Deliver() => throw new NotImplementedException();
}

The builders are almost equally trivial:

internal abstract class BookBuilder : IBookBuilder
{
    public int Id { get; }

    public string? Title { get; set; }

    public List<string> Authors { get; } = new();

    protected BookBuilder( int id )
    {
        this.Id = id;
    }

    public abstract IBook Build();
}

internal class PaperBookBuilder : BookBuilder
{
    public PaperBookBuilder( int id ) : base( id ) { }

    public override IBook Build() => new PaperBook( this.Id, this.Title!, [..this.Authors] );
}

internal class EBookBuilder : BookBuilder
{
    public EBookBuilder( int id ) : base( id ) { }

    public override IBook Build() => new EBook( this.Id, this.Title!, [..this.Authors] );
}

Finally, here is the top-level class, the one added to the IServiceProvider:

internal class BookBuilderFactory( IIdGenerator idGenerator ) : IBookFactory
{
    public IBookBuilder CreateBookBuilder( BookKind kind )
    {
        var id = idGenerator.Next();

        return kind switch
        {
            BookKind.Paper => new PaperBookBuilder( id ),
            BookKind.EBook => new EBookBuilder( id ),
            _ => throw new ArgumentException()
        };
    }
}

Use case 3: Async creation of objects

Another use case is when the creation of the object requires an asynchronous call. Since there is no async constructor in C#, you must use either an async Factory pattern or an async Builder pattern.

As an example, suppose that books have an Id property whose value must be uniquely generated by the database.

public record Book( int Id, string Title );

internal interface IDatabaseIdGenerator
{
    Task<int> NextIdAsync();
}

public class BookBuilder
{
    private readonly IDatabaseIdGenerator _idGenerator;

    internal BookBuilder( IDatabaseIdGenerator idGenerator )
    {
        this._idGenerator = idGenerator;
    }

    public string? Title { get; set; }

    public async Task<Book> BuildAsync()
    {
        var id = await this._idGenerator.NextIdAsync();

        return new Book( id, this.Title! );
    }
}

Alternatives to the Builder pattern

  • Since C# 9.0, record types make it easier to build immutable objects. Thanks to init fields and properties, you no longer need mammoth constructors, so one of the traditional justifications of the Builder pattern falls. Last but not least, the with keyword allows you to perform incremental modifications of immutable objects. The downside of the with keyword is that each use instantiates a new object, so it might add some performance overhead for initializations that require many steps. This overhead may be negligible when this happens only at application startup.

  • The Factory pattern and the Abstract Factory pattern can also be used to instantiate immutable objects, but these patterns do not allow for multi-step initializations.

  • The Freezable pattern combines the benefits of mutability during initialization and immutability after initialization.

How to implement the Builder pattern in C#?

You can either implement the Builder pattern manually or automate its implementation using meta-programming. In both cases, you should first define an implementation strategy and ensure that everyone on the team always uses the same strategy.

Requirements

Most of the time, you will want your Builder pattern to support the following features:

  • The builder object must always have a Build() method that creates the immutable object.
  • The builder object may have a Validate() method called by the Build() method to validate the object integrity before creating an instance of the immutable type.
  • The immutable object may have a ToBuilder() method that creates a new builder initialized with the current values.
  • Collection properties of the builder type must be mutable. For instance, if the immutable object has a property of type ImmutableArray<string> or IReadOnlyList<string>, the builder object must have a corresponding property of type ImmutableArray<string>.Builder or List<string>.
  • The implementation pattern must support type inheritance.

Example

Consider the following immutable classes:

public partial record Book( string Title, ImmutableArray<string> Authors );

public partial record EBook( string Title, ImmutableArray<string> Authors, string Url )
    : Book( Title, Authors );

The implementation of the Builder pattern for Book, the base class, is the following:

public partial record Book
{
    public virtual Builder ToBuilder() => new( this );

    public class Builder
    {
        public string? Title { get; set; }

        public ImmutableArray<string>.Builder Authors { get; set; }

        public Builder()
        {
            this.Authors = ImmutableArray.CreateBuilder<string>();
        }

        public Builder( Book prototype )
        {
            this.Title = prototype.Title;
            this.Authors = prototype.Authors.ToBuilder();
        }

        protected virtual void Validate()
        {
            if ( this.Title == null )
            {
                throw new ValidationException( "The book title cannot be null." );
            }

            if ( this.Authors.Count == 0 )
            {
                throw new ValidationException( "The book author cannot be null." );
            }
        }

        protected virtual Book Build()
        {
            this.Validate();

            return new Book( this.Title!, this.Authors.ToImmutableArray() );
        }
    }
}

Note the following points in the above snippet:

  • We used an ImmutableArray<string>.Builder to represent the collection of authors in the builder class.
  • We included a copy constructor in the build class, copying values from the Book class into Book.Builder.

Let’s now implement the pattern for the derived class to check that our pattern is sound for class inheritance.

public partial record EBook
{
    public override Builder ToBuilder() => new( this );

    public new class Builder : Book.Builder
    {
        public string? Url { get; set; }

        public Builder() { }

        public Builder( EBook prototype ) : base( prototype )
        {
            this.Url = prototype.Url;
        }

        protected override void Validate()
        {
            base.Validate();

            if ( this.Url == null )
            {
                throw new ValidationException( "The book Url cannot be null." );
            }
        }

        protected override EBook Build()
        {
            base.Validate();

            return new EBook( this.Title!, this.Authors.ToImmutableArray(), this.Url! );
        }
    }
}

How to automate its implementation?

If you feel that the Builder pattern requires a lot of boilerplate code, you’re absolutely right. Although the pattern is very convenient for users of your code, it’s a pain for the author. Unless, of course, you automate its implementation.

Two technologies can generate the code for you: Roslyn code generators and Metalama. Metalama is itself based on Roslyn, including code generators. While code generators are string-oriented, Metalama offers a real object model for meta-programming.

With Metalama, you’re done in three steps:

  1. Add a package reference:

     <ItemGroup>
         <PackageReference Include="Metalama.Framework"/>
     </ItemGroup>
    
  2. Copy into your project the aspect code developed in the article Implementing the Builder pattern with Metalama. The code is available on GitHub. It’s not the simplest aspect, but you can read the source code and adapt it to your needs.
  3. Since the aspect is a custom attribute, you can it to your code like this:

     [GenerateBuilder]
     public partial record Book(string Title, ImmutableArray<string> Authors);
    

And you’re done! The Builder code is now generated automatically both at build time and as you type in the editor.

What is the Director in the Builder design pattern?

You may have heard of a component of the Builder pattern called the Director, mentioned in the GoF book.

You don’t necessarily need a Director when using the Builder pattern. In this article, we have shown simple examples of single-part objects. The Director is useful when you build a multi-part object and need to orchestrate the work of several builders or build methods.

Conclusion

The Builder pattern is one of the most common in C#, especially in recent years, as the functional style relying on immutability has gained popularity. This pattern has many benefits, effectively decoupling the initialization of an object from its use. It is both more flexible and complex than patterns such as Factory or Abstract Factory. A traditional use case (avoiding mammoth constructors) has been made obsolete by recent C# features like records, init properties, or required members. Yet, it remains very useful.

The problem with the Builder pattern is the sheer amount of boilerplate code it requires. Fortunately, you can use meta-programming to automate the generation of this code. In this article, we mentioned using Metalama. The implementation of the Builder pattern using Metalama is detailed in a separate article.

This article was first published on a https://blog.postsharp.net under the title The Builder Pattern in C# [2024].

Discover Metalama, the leading code generation and validation toolkit for C#

  • Write and maintain less code by eliminating boilerplate, generating it dynamically during compilation, typically reducing code lines and bugs by 15%.
  • Validate your codebase against your own rules in real-time to enforce adherence to your architecture, patterns, and conventions. No need to wait for code reviews.
  • Excel with large, complex, or old codebases. Metalama does not require you to change your architecture. Beyond getting started, it's at scale that it really shines.

Discover Metalama Free Edition

Related articles