BrandGhost
How to Implement Builder Pattern in C#: Step-by-Step Guide

How to Implement Builder Pattern in C#: Step-by-Step Guide

How to Implement Builder Pattern in C#: Step-by-Step Guide

Implementing the Builder pattern in C# requires understanding its structure and following a systematic approach. This step-by-step guide will walk you through implementing Builder pattern in C# from scratch, with complete code examples and best practices for C# development.

Whether you're building configuration objects, query builders, or any application requiring flexible object construction, this guide provides the practical knowledge you need to implement Builder correctly.

This article focuses on: Step-by-step implementation mechanics and C# implementation details. This is the canonical implementation guide for the cluster—other articles reference this for detailed code structure. For conceptual foundation, see Builder Design Pattern: Complete Guide. For decision-making guidance, see When to Use Builder Pattern. For advanced practices, see Builder Pattern Best Practices.

For conceptual understanding and working examples, explore The Big List of Design Patterns.

Step 1: Identify Your Product

Proper planning is essential for a successful Builder implementation. Before writing any code, clearly define what you're building and what properties it needs.

Before writing code, identify:

  1. What object are you building? (e.g., Computer, Email, Query)
  2. What properties does it have? (e.g., CPU, RAM, Storage)
  3. Which properties are required vs optional? (e.g., CPU required, GPU optional)
  4. What validation is needed? (e.g., RAM must be valid size)

Example: Email Builder

To illustrate the implementation process, let's use an email builder as a concrete example. This example demonstrates how to implement Builder pattern in C# for a common use case.

Product: Email message
Properties: To, From, Subject, Body, Attachments, Priority
Required: To, From, Subject
Optional: Body, Attachments, Priority

Step 2: Define the Product Class

The foundation of Builder is defining the product class that will be constructed. Create a class representing the complex object:

// Step 2: Product Class
public class Email
{
    public string To { get; set; }
    public string From { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
    public List<string> Attachments { get; set; } = new();
    public EmailPriority Priority { get; set; } = EmailPriority.Normal;
    
    public void Send()
    {
        // Email sending logic
        Console.WriteLine($"Sending email to {To} with subject: {Subject}");
    }
}

public enum EmailPriority
{
    Low,
    Normal,
    High
}

Key Points:

  • Properties can be mutable (as shown) or immutable (using readonly properties)
  • Initialize collections in the property declaration
  • Set sensible defaults where appropriate

Step 3: Create the Builder Interface

The builder interface defines the contract for constructing the product. Each method should return the builder itself to enable method chaining:

// Step 3: Builder Interface
public interface IEmailBuilder
{
    IEmailBuilder SetTo(string to);
    IEmailBuilder SetFrom(string from);
    IEmailBuilder SetSubject(string subject);
    IEmailBuilder SetBody(string body);
    IEmailBuilder AddAttachment(string attachment);
    IEmailBuilder SetPriority(EmailPriority priority);
    Email Build();
}

Key Points:

  • Methods return IEmailBuilder for method chaining
  • Build() method returns the final product
  • Method names should be clear and descriptive

Step 4: Implement the Concrete Builder

The concrete builder is where the actual implementation of Builder pattern in C# happens. This class implements the builder interface and manages the internal state of the product being constructed. It handles validation, state management, and the final construction logic.

The concrete builder implements the interface and manages the construction state:

// Step 4: Concrete Builder
public class EmailBuilder : IEmailBuilder
{
    private Email _email = new();
    
    public IEmailBuilder SetTo(string to)
    {
        if (string.IsNullOrWhiteSpace(to))
            throw new ArgumentException("To address cannot be empty", nameof(to));
            
        _email.To = to;
        return this;
    }
    
    public IEmailBuilder SetFrom(string from)
    {
        if (string.IsNullOrWhiteSpace(from))
            throw new ArgumentException("From address cannot be empty", nameof(from));
            
        _email.From = from;
        return this;
    }
    
    public IEmailBuilder SetSubject(string subject)
    {
        if (string.IsNullOrWhiteSpace(subject))
            throw new ArgumentException("Subject cannot be empty", nameof(subject));
            
        _email.Subject = subject;
        return this;
    }
    
    public IEmailBuilder SetBody(string body)
    {
        _email.Body = body ?? string.Empty;
        return this;
    }
    
    public IEmailBuilder AddAttachment(string attachment)
    {
        if (!string.IsNullOrWhiteSpace(attachment))
        {
            _email.Attachments.Add(attachment);
        }
        return this;
    }
    
    public IEmailBuilder SetPriority(EmailPriority priority)
    {
        _email.Priority = priority;
        return this;
    }
    
    public Email Build()
    {
        // Validate required fields
        if (string.IsNullOrWhiteSpace(_email.To))
            throw new InvalidOperationException("To address is required");
            
        if (string.IsNullOrWhiteSpace(_email.From))
            throw new InvalidOperationException("From address is required");
            
        if (string.IsNullOrWhiteSpace(_email.Subject))
            throw new InvalidOperationException("Subject is required");
        
        // Create and return the email
        var result = _email;
        _email = new Email(); // Reset for next build
        return result;
    }
}

Key Points:

  • Store the product being built as a private field
  • Validate inputs in setter methods
  • Validate required fields in Build()
  • Reset the builder state after building (if reusing)

Step 5: Use the Builder

Now you can use the builder to construct objects:

// Step 5: Usage
var email = new EmailBuilder()
    .SetTo("[email protected]")
    .SetFrom("[email protected]")
    .SetSubject("Important Update")
    .SetBody("This is the email body.")
    .AddAttachment("document.pdf")
    .SetPriority(EmailPriority.High)
    .Build();
    
email.Send();

Advanced Implementation: Immutable Product

When implementing Builder pattern in C# for immutable objects, you need a different approach. Immutable objects provide thread safety and prevent accidental modifications, but they require careful design to work with Builder pattern effectively.

For immutable products, use a private constructor and nested builder:

// Immutable Product with Nested Builder
public class ImmutableEmail
{
    public string To { get; }
    public string From { get; }
    public string Subject { get; }
    public string Body { get; }
    public IReadOnlyList<string> Attachments { get; }
    public EmailPriority Priority { get; }
    
    // Private constructor - only Builder can create instances
    private ImmutableEmail(
        string to,
        string from,
        string subject,
        string body,
        List<string> attachments,
        EmailPriority priority)
    {
        To = to;
        From = from;
        Subject = subject;
        Body = body;
        Attachments = attachments.AsReadOnly();
        Priority = priority;
    }
    
    // Nested Builder class
    public class Builder
    {
        private string _to;
        private string _from;
        private string _subject;
        private string _body;
        private List<string> _attachments = new();
        private EmailPriority _priority = EmailPriority.Normal;
        
        public Builder SetTo(string to)
        {
            _to = to ?? throw new ArgumentNullException(nameof(to));
            return this;
        }
        
        public Builder SetFrom(string from)
        {
            _from = from ?? throw new ArgumentNullException(nameof(from));
            return this;
        }
        
        public Builder SetSubject(string subject)
        {
            _subject = subject ?? throw new ArgumentNullException(nameof(subject));
            return this;
        }
        
        public Builder SetBody(string body)
        {
            _body = body ?? string.Empty;
            return this;
        }
        
        public Builder AddAttachment(string attachment)
        {
            if (!string.IsNullOrWhiteSpace(attachment))
            {
                _attachments.Add(attachment);
            }
            return this;
        }
        
        public Builder SetPriority(EmailPriority priority)
        {
            _priority = priority;
            return this;
        }
        
        public ImmutableEmail Build()
        {
            // Validate required fields
            if (string.IsNullOrWhiteSpace(_to))
                throw new InvalidOperationException("To address is required");
            if (string.IsNullOrWhiteSpace(_from))
                throw new InvalidOperationException("From address is required");
            if (string.IsNullOrWhiteSpace(_subject))
                throw new InvalidOperationException("Subject is required");
            
            // Create immutable instance
            return new ImmutableEmail(
                _to,
                _from,
                _subject,
                _body,
                _attachments,
                _priority);
        }
    }
}

// Usage
var email = new ImmutableEmail.Builder()
    .SetTo("[email protected]")
    .SetFrom("[email protected]")
    .SetSubject("Important Update")
    .SetBody("Email body")
    .Build();

Advanced Implementation: Fluent Builder with Validation

Add validation logic throughout the building process:

public class ValidatedEmailBuilder : IEmailBuilder
{
    private Email _email = new();
    private readonly List<string> _validationErrors = new();
    
    public IEmailBuilder SetTo(string to)
    {
        if (string.IsNullOrWhiteSpace(to))
        {
            _validationErrors.Add("To address is required");
        }
        else if (!IsValidEmail(to))
        {
            _validationErrors.Add($"Invalid email address: {to}");
        }
        else
        {
            _email.To = to;
        }
        return this;
    }
    
    public IEmailBuilder SetFrom(string from)
    {
        if (string.IsNullOrWhiteSpace(from))
        {
            _validationErrors.Add("From address is required");
        }
        else if (!IsValidEmail(from))
        {
            _validationErrors.Add($"Invalid email address: {from}");
        }
        else
        {
            _email.From = from;
        }
        return this;
    }
    
    public IEmailBuilder SetSubject(string subject)
    {
        if (string.IsNullOrWhiteSpace(subject))
        {
            _validationErrors.Add("Subject is required");
        }
        else if (subject.Length > 200)
        {
            _validationErrors.Add("Subject cannot exceed 200 characters");
        }
        else
        {
            _email.Subject = subject;
        }
        return this;
    }
    
    public IEmailBuilder SetBody(string body)
    {
        _email.Body = body ?? string.Empty;
        return this;
    }
    
    public IEmailBuilder AddAttachment(string attachment)
    {
        if (!string.IsNullOrWhiteSpace(attachment))
        {
            if (!File.Exists(attachment))
            {
                _validationErrors.Add($"Attachment not found: {attachment}");
            }
            else
            {
                _email.Attachments.Add(attachment);
            }
        }
        return this;
    }
    
    public IEmailBuilder SetPriority(EmailPriority priority)
    {
        _email.Priority = priority;
        return this;
    }
    
    public Email Build()
    {
        if (_validationErrors.Count > 0)
        {
            throw new InvalidOperationException(
                $"Validation failed:
{string.Join("
", _validationErrors)}");
        }
        
        var result = _email;
        _email = new Email();
        _validationErrors.Clear();
        return result;
    }
    
    private bool IsValidEmail(string email)
    {
        try
        {
            var addr = new System.Net.Mail.MailAddress(email);
            return addr.Address == email;
        }
        catch
        {
            return false;
        }
    }
}

Common Implementation Patterns

When implementing Builder pattern in C#, there are several common patterns that appear across different implementations. Understanding these patterns helps you write consistent, maintainable builder code.

Pattern 1: Method Chaining

Method chaining is fundamental to Builder pattern in C#. By returning the builder instance from each method, you enable a fluent interface that reads naturally.

Always return this or the interface type to enable fluent syntax:

public IEmailBuilder SetTo(string to)
{
    _email.To = to;
    return this; // Enables chaining
}

Pattern 2: Reset After Build

If reusing builders, reset state:

public Email Build()
{
    var result = _email;
    _email = new Email(); // Reset
    return result;
}

Pattern 3: Validation in Build

The Build() method is the final checkpoint when implementing Builder pattern in C#. This is where you should validate that all required fields are set and that the combination of values is valid before constructing the final object.

Validate all required fields in Build():

public Email Build()
{
    if (string.IsNullOrWhiteSpace(_email.To))
        throw new InvalidOperationException("To is required");
    // ... more validation
    
    return _email;
}

Testing Your Builder

Write unit tests to verify your builder works correctly:

[Test]
public void Build_WithRequiredFields_ReturnsEmail()
{
    // Arrange
    var builder = new EmailBuilder();
    
    // Act
    var email = builder
        .SetTo("[email protected]")
        .SetFrom("[email protected]")
        .SetSubject("Test")
        .Build();
    
    // Assert
    Assert.AreEqual("[email protected]", email.To);
    Assert.AreEqual("[email protected]", email.From);
    Assert.AreEqual("Test", email.Subject);
}

[Test]
public void Build_WithoutRequiredFields_ThrowsException()
{
    // Arrange
    var builder = new EmailBuilder();
    
    // Act & Assert
    Assert.Throws<InvalidOperationException>(() => builder.Build());
}

[Test]
public void Build_WithOptionalFields_IncludesThem()
{
    // Arrange
    var builder = new EmailBuilder();
    
    // Act
    var email = builder
        .SetTo("[email protected]")
        .SetFrom("[email protected]")
        .SetSubject("Test")
        .SetBody("Body")
        .AddAttachment("file.pdf")
        .SetPriority(EmailPriority.High)
        .Build();
    
    // Assert
    Assert.AreEqual("Body", email.Body);
    Assert.AreEqual(1, email.Attachments.Count);
    Assert.AreEqual(EmailPriority.High, email.Priority);
}

Integration with Dependency Injection

When implementing Builder pattern in C# within a dependency injection framework, you can register builders as services. This allows you to inject builders into your classes, promoting loose coupling and testability.

Builders can be registered in your DI container:

// Register builder in DI container
services.AddTransient<IEmailBuilder, EmailBuilder>();

// Use in your services
public class EmailService
{
    private readonly IEmailBuilder _builder;
    
    public EmailService(IEmailBuilder builder)
    {
        _builder = builder;
    }
    
    public void SendWelcomeEmail(string to)
    {
        var email = _builder
            .SetTo(to)
            .SetFrom("[email protected]")
            .SetSubject("Welcome!")
            .SetBody("Welcome to our service!")
            .Build();
            
        email.Send();
    }
}

Conclusion

Implementing the Builder pattern in C# follows a clear, systematic approach: define your product, create a builder interface, implement the concrete builder, and use it to construct objects. The pattern's fluent interface style fits naturally with modern C# development, and the separation of construction from representation provides flexibility and maintainability.

Remember to validate inputs, handle required vs optional parameters appropriately, and consider immutability when designing your products. With these steps, you can implement Builder effectively in your C# applications.

Frequently Asked Questions

Should I use an interface or abstract class for the Builder?

Use an interface when you want maximum flexibility and multiple implementations. Use an abstract class when you want to share common implementation code between builders.

How do I handle collections in Builder pattern?

Initialize collections in the product class, then add items through builder methods. Return the builder from add methods to enable chaining.

Can I reuse a Builder instance?

Yes, but you must reset the internal state in the Build() method. Otherwise, subsequent builds will include data from previous builds.

What's the difference between Builder and object initializers?

Object initializers are simpler but don't support validation, step-by-step construction, or different representations. Builder provides more control and flexibility.

Should Build() always create a new instance?

Not necessarily. For mutable products, you might return the same instance. For immutable products, always create a new instance. The key is ensuring the builder can be reused safely.

How do I handle optional parameters with defaults?

Set default values in the product class property initialization, or check for null/empty in the Build() method and apply defaults there.

Can Builder methods be async?

Yes, but it complicates the fluent interface. Consider using async/await in Build() if you need asynchronous validation or initialization.

The Builder Pattern in C#: How To Leverage Extension Methods Creatively

If you want to see examples of the builder pattern in C#, dive into this article. We'll explore how the builder pattern in C# works with code examples!

How to Implement Abstract Factory Pattern in C#: Step-by-Step Guide

Learn how to implement Abstract Factory pattern in C# with a complete step-by-step guide. Includes code examples, best practices, and common pitfalls to avoid.

Builder Design Pattern in C#: Complete Guide with Examples

Master the Builder design pattern in C# with code examples, real-world scenarios, and implementation guidance for constructing complex objects step by step.

An error has occurred. This application may no longer respond until reloaded. Reload x