Fluent Builder Pattern in C#

Programming C#

The Fluent Builder pattern is a powerful design pattern that enables the creation of complex objects through a more readable and maintainable interface.

Why Use the Fluent Builder Pattern?

Before we dive into implementations, let’s understand why you might want to use the Fluent Builder pattern:

  • Improves code readability through method chaining
  • Separates the construction of complex objects from their representation
  • Enforces immutability while maintaining flexible object construction
  • Handles optional parameters elegantly
  • Provides a clear API for object construction

Real-World Example: StringBuilder

The StringBuilder class in .NET is an excellent example of the Fluent Builder pattern:

var message = new StringBuilder()
    .Append("Hello")
    .Append(" ")
    .Append("World")
    .AppendLine("!")
    .ToString();

ReBuild the HttpClient Class

Let's look at how the HttpClient class is being used:

var client = new HttpClient(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(10),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
    MaxConnectionsPerServer = 10
});

While this works, it could be more elegant. Here’s how we could implement a fluent builder for HttpClient:

public class HttpClientBuilder
{
    private readonly SocketsHttpHandler _handler;
    
    private HttpClientBuilder()
    {
        _handler = new SocketsHttpHandler();
    }
    
    public static HttpClientBuilder Create()
    {
        return new HttpClientBuilder();
    }
    
    public HttpClientBuilder WithConnectionLifetime(TimeSpan lifetime)
    {
        _handler.PooledConnectionLifetime = lifetime;
        return this;
    }
    
    public HttpClientBuilder WithIdleTimeout(TimeSpan timeout)
    {
        _handler.PooledConnectionIdleTimeout = timeout;
        return this;
    }
    
    public HttpClientBuilder WithMaxConnections(int maxConnections)
    {
        _handler.MaxConnectionsPerServer = maxConnections;
        return this;
    }
    
    public HttpClient Build()
    {
        return new HttpClient(_handler);
    }
}

Now we can create an HttpClient in a much more elegant way:

var client = HttpClientBuilder.Create()
    .WithConnectionLifetime(TimeSpan.FromMinutes(10))
    .WithIdleTimeout(TimeSpan.FromMinutes(5))
    .WithMaxConnections(10)
    .Build();

Advanced Pattern: Generic Builders with Constraints

Sometimes you need to create builders that work with generic types. Here’s an example of a generic collection builder:

public class CollectionBuilder<T> where T : class
{
    private readonly List<T> _items = new List<T>();
    private readonly List<Action<List<T>>> _configurations = new List<Action<List<T>>>();

    public CollectionBuilder<T> Add(T item)
    {
        _items.Add(item);
        return this;
    }

    public CollectionBuilder<T> AddRange(IEnumerable<T> items)
    {
        _items.AddRange(items);
        return this;
    }

    public CollectionBuilder<T> Configure(Action<List<T>> configuration)
    {
        _configurations.Add(configuration);
        return this;
    }

    public CollectionBuilder<T> WithFilter(Func<T, bool> predicate)
    {
        _configurations.Add(items => 
        {
            var itemsToRemove = items.Where(x => !predicate(x)).ToList();
            foreach (var item in itemsToRemove)
            {
                items.Remove(item);
            }
        });
        return this;
    }

    public IReadOnlyList<T> Build()
    {
        foreach (var configuration in _configurations)
        {
            configuration(_items);
        }
        return _items.AsReadOnly();
    }
}

Consume it like this:

var numbers = new CollectionBuilder<string>()
    .Add("1")
    .Add("2")
    .Add("3")
    .AddRange(new[] { "4", "5", "6" })
    .Configure(list => list.Sort())
    .WithFilter(x => int.Parse(x) > 2)
    .Build();

Best Practices and Guidelines

When implementing the Fluent Builder pattern, consider these best practices:

  • Make the final object immutable
  • Use descriptive method names that start with “With” or similar prefixes
  • Return the builder instance from configuration methods
  • Provide a clear Build() method
  • Consider making the builder’s constructor private and providing a static creation method
  • Use method chaining for a more natural API
  • Implement validation in the Build() method
  • Consider adding a Reset() method for builder reuse

Common Pitfalls to Avoid

  • Overcomplicating simple object creation
  • Not handling null values properly
  • Making the builder mutable after building
  • Not implementing proper validation
  • Creating too many nested builders
  • Not providing a way to reset the builder

When to Use the Fluent Builder Pattern

The Fluent Builder pattern is most useful when:

  • Objects require many parameters for construction
  • Object construction involves multiple steps
  • You need to enforce immutability
  • You want to provide a more readable API
  • You need to construct objects with optional parameters
  • You’re working with complex nested objects