Builder Pattern with Implicit Conversion in C#

TL;DR Use implicit conversion in your Builder Pattern, so the final Build() method call can be omitted.

The source code is available on GitHub.

Builder Design Pattern

Builder Design Pattern simplifies the process of creating complex objects with numerous properties or configurations, all while enhancing code readability.

At the heart of its effectiveness is the fluent interface, providing a user-friendly way to build objects with a natural flow, in a step-by-step fashion, resulting in code that reads like a clear and expressive narrative.

// Without Builder Pattern
User user = new User("John", "Doe", 25, new Authentication("jdoe", "password123"));

// With Builder Pattern
User user = new UserBuilder()
    .WithFirstName("John")
    .WithLastName("Doe")
    .WithAge(25)
    .WithAuthentication(
        new AuthenticationBuilder()
            .WithUsername("jdoe")
            .WithPassword("password123")
            .Build()
    )
    .Build();

After setting up the properties, the Builder class wraps things up with Build() method to create the final object.

Implicit conversion

With implicit conversion, you can use the builder directly where the built object is expected, eliminating the need for an explicit Build() method call.

// No `Build()` call needed
Authentication authentication = new AuthenticationBuilder()
    .WithUsername("jdoe")
    .WithPassword("password123");

// => "Authentication: jdoe, password123"
Console.WriteLine($"Authentication: {authentication.Username}, {authentication.Password}");

Here's how you might implement implicit conversion in the context of Authentication:

public class AuthenticationBuilder
{
    // ... Other builder methods ...

    // Implicit conversion from `AuthenticationBuilder` to `Authentication`
    public static implicit operator Authentication(AuthenticationBuilder builder)
    {
        return builder.Build();
    }
}

Base class for shared functionality

Common methods, such as Build() and the implicit conversion, can be implemented in the base class, reducing the need to duplicate this logic in every builder class. By centralizing common methods for building instances, any changes or improvements to the build process can be applied in a single place.

public abstract class BaseBuilder<T> where T : class
{
    protected abstract T Instance { get; }

    // Implicit conversion from `BaseBuilder<T>` to the final `T` object
    public static implicit operator T(BaseBuilder<T> builder)
    {
        return builder.Build();
    }

    public virtual T Build()
    {
        return Instance;
    }
}

public class AuthenticationBuilder : BaseBuilder<Authentication>
{
    // ... Other builder methods ...
}

// Usage
Authentication authentication = new AuthenticationBuilder().WithUsername("jdoe");

Real-world example

In real-world scenarios, the Builder Pattern is particularly useful when setting up the default, initial state, while also offering the flexibility to override individual properties as needed.

Its fluent and expressive nature not only improves code readability, but also makes it easier for developers to grasp the intent of each test case.

// Builds default User, but overrides first name
User user = TestHelper.BuildUserWithAuthentication(userBuilder => 
    userBuilder.WithFirstName("Jane")
);

// => "User: Jane, Doe, 25"
Console.WriteLine($"User: {user.FirstName}, {user.LastName}, {user.Age}");

public static class TestHelper
{
    public static User BuildUserWithAuthentication(Action<UserBuilder>? userBuilder = null)
    {
        // Default user
        UserBuilder user = new UserBuilder()
            .WithFirstName("John")
            .WithLastName("Doe")
            .WithAge(25)
            .WithAuthentication(
                new AuthenticationBuilder()
                    .WithUsername("john")
                    .WithPassword("password123")
            );

        // Apply overrides if any
        userBuilder?.Invoke(user);

        return user.Build();
    }
}

Resources