Implementing the Builder Design Pattern in C#

Classic Implementation

Definision

The Builder Pattern decouples the construction of a complex object from its representation, allowing the same construction process to create different representations.

Pattern Structure

  • Abstract Builder (Builder): Defines an abstract interface for creating parts of a Product object. It specifies what needs to be built but not how.
  • Concrete Builder: Implements the builder interface. It constructs and assembles parts of the product, defines the specific representation, and provides a way to retrieve the constructed object.
  • Director: Constructs an object using the Builder interface. It knows the sequence of assembly but is unaware of the specific product details.
  • Product: The complex object under construction. It includes classes that define the components and their asseembly.

Example: Assembling Vehicles

Product and Abstract Builder

// The complex object we want to build
public class Vehicle
{
    public IList<string> Wheels { get; set; }
    public IList<string> Lights { get; set; }
}

// Abstract builder defining the steps
public abstract class VehicleAssembler
{
    public Vehicle CurrentVehicle { get; protected set; }

    public void Initialize()
    {
        CurrentVehicle = new Vehicle();
    }

    public abstract void InstallWheels();
    public abstract void InstallLights();
}

Concrete Builders

// Builder for a Car
public class CarAssembler : VehicleAssembler
{
    public override void InstallWheels()
    {
        CurrentVehicle.Wheels = new List<string> { "FL", "FR", "RL", "RR" };
    }

    public override void InstallLights()
    {
        CurrentVehicle.Lights = new List<string> { "Headlight", "Headlight", "Taillight", "Taillight" };
    }
}

// Builder for a Bicycle
public class BikeAssembler : VehicleAssembler
{
    public override void InstallWheels()
    {
        CurrentVehicle.Wheels = new List<string> { "Front", "Rear" };
    }

    public override void InstallLights()
    {
        CurrentVehicle.Lights = null; // Bicycles might not have lights
    }
}

Director

// The Director orchestrates the building process
public class AssemblyLine
{
    public Vehicle Assemble(VehicleAssembler assembler)
    {
        assembler.Initialize();
        assembler.InstallWheels();
        assembler.InstallLights();
        return assembler.CurrentVehicle;
    }
}

Usage

var line = new AssemblyLine();

var car = line.Assemble(new CarAssembler());
// car.Wheels.Count == 4

var bike = line.Assemble(new BikeAssembler());
// bike.Wheels.Count == 2

Pros and Cons

Advantages:

  • Allows fine-grained control over the construction steps and their order.
  • Isolates complex construction code from the business logic.
  • Supports constructing different representations using the same process.

Disadvantages:

  • Increases code complexity by introducing multiple new classes.
  • The Director can become a bottleneck if the construction logic changes frequently.

Attribute-Driven Builder

This approach uses custom attributes and reflection to define the build steps directly on the product class, eliminating the need for separate Builder classes.

Attribute Definition

[AttributeUsage(AttributeTargets.Method)]
public class AssemblyStepAttribute : Attribute
{
    public int Order { get; }
    public int RepeatCount { get; }

    public AssemblyStepAttribute(int order, int count = 1)
    {
        Order = order;
        RepeatCount = count;
    }
}

Reflection Helper

public class StepScanner
{
    static Dictionary<Type, List<MethodInfo>> _cache = new Dictionary<Type, List<MethodInfo>>();

    public List<MethodInfo> GetSteps(Type type)
    {
        if (_cache.ContainsKey(type)) return _cache[type];

        var methods = type.GetMethods()
            .Where(m => m.GetCustomAttribute<AssemblyStepAttribute>() != null)
            .OrderBy(m => m.GetCustomAttribute<AssemblyStepAttribute>().Order)
            .ToList();

        _cache[type] = methods;
        return methods;
    }
}

Generic Builder

public class DynamicBuilder<T> where T : new()
{
    public T Construct()
    {
        var scanner = new StepScanner();
        var steps = scanner.GetSteps(typeof(T));
        T instance = new T();

        foreach (var step in steps)
        {
            var attr = step.GetCustomAttribute<AssemblyStepAttribute>();
            for (int i = 0; i < attr.RepeatCount; i++)
            {
                step.Invoke(instance, null);
            }
        }
        return instance;
    }
}

Product with Attributes

class Sedan
{
    public List<string> Components { get; set; } = new List<string>();

    [AssemblyStep(1)]
    public void AttachChassis() => Components.Add("Chassis");

    [AssemblyStep(2, 4)]
    public void AddTire() => Components.Add("Tire");
}

Reversible Builder (Teardown)

Sometimes objects need to be dismantled as well as built. This interface supports both operations.

public interface IReversibleBuilder<T>
{
    T Assemble();
    T Dismantle();
}

public class DeviceBuilder : IReversibleBuilder<Device>
{
    private Device _device = new Device();

    public Device Assemble()
    {
        _device.Parts = new List<string> { "CPU", "RAM", "SSD" };
        return _device;
    }

    public Device Dismantle()
    {
        _device.Parts.Clear();
        return _device;
    }
}

public class Device { public List<string> Parts { get; set; } }

Fluent Builder (Chaining)

The Fluent interface allows for readable, chained method calls during object creation. This is useful for objects with many optional parameters.

public class ServerConfig
{
    public string IP { get; }
    public int Port { get; }
    public bool IsSecure { get; }

    private ServerConfig(Builder builder)
    {
        IP = builder.IPAddress;
        Port = builder.PortNumber;
        IsSecure = builder.Secure;
    }

    public class Builder
    {
        public string IPAddress { get; private set; }
        public int PortNumber { get; private set; }
        public bool Secure { get; private set; }

        public Builder UseIP(string ip)
        {
            IPAddress = ip;
            return this;
        }

        public Builder OnPort(int port)
        {
            PortNumber = port;
            return this;
        }

        public Builder EnableSSL()
        {
            Secure = true;
            return this;
        }

        public ServerConfig Build()
        {
            return new ServerConfig(this);
        }
    }
}

// Usage:
// var config = new ServerConfig.Builder().UseIP("127.0.0.1").OnPort(8080).EnableSSL().Build();

Event-Based Builder (AOP Style)

By exposing events during the construction liefcycle, we can inject cross-cutting concerns like logging or validation without modifying the builder logic.

public class ObservableBuilder<T> where T : new()
{
    public event Action<T> OnConstructionStarted;
    public event Action<T, string> OnStepCompleted;

    public T Build()
    {
        T instance = new T();
        OnConstructionStarted?.Invoke(instance);

        // Simulate steps
        AddComponent(instance, "Engine");
        AddComponent(instance, "Wheels");
        
        return instance;
    }

    private void AddComponent(T target, string component)
    {
        // Logic to add component...
        OnStepCompleted?.Invoke(target, component);
    }
}

Tags: C# Builder Pattern Design Patterns reflection attributes

Posted on Wed, 19 Aug 2026 16:44:28 +0000 by m@tt