Backend Systemsbeginner
Builder Pattern in C#
Learn the Builder pattern for constructing complex objects step-by-step. See fluent builder APIs, test data builders, and query builders in real C# code.
Asma HafeezApril 17, 20264 min read
csharpdesign-patternsbuilderdotnetfluent-api
Builder Pattern
Builder constructs complex objects step-by-step. Instead of a constructor with 10 parameters (hard to read, error-prone), you get a fluent API that reads like a sentence.
The Problem
C#
// Confusing — what does each parameter mean?
var email = new Email("alice@example.com", "bob@example.com", "Welcome!", "<h1>Hi</h1>", true, 3, null, "urgent");
// Better with named parameters... but still not great at scale
var email = new Email(
from: "alice@example.com",
to: "bob@example.com",
subject: "Welcome!",
body: "<h1>Hi</h1>",
isHtml: true,
retryCount: 3,
cc: null,
priority: "urgent"
);Fluent Builder
C#
public class Email
{
public string From { get; init; } = string.Empty;
public string To { get; init; } = string.Empty;
public string Subject { get; init; } = string.Empty;
public string Body { get; init; } = string.Empty;
public bool IsHtml { get; init; }
public string Priority { get; init; } = "normal";
public List<string> Cc { get; init; } = [];
}
public class EmailBuilder
{
private string _from = string.Empty;
private string _to = string.Empty;
private string _subject = string.Empty;
private string _body = string.Empty;
private bool _isHtml = false;
private string _priority = "normal";
private readonly List<string> _cc = [];
public EmailBuilder From(string from) { _from = from; return this; }
public EmailBuilder To(string to) { _to = to; return this; }
public EmailBuilder WithSubject(string sub) { _subject = sub; return this; }
public EmailBuilder WithBody(string body) { _body = body; return this; }
public EmailBuilder AsHtml() { _isHtml = true; return this; }
public EmailBuilder Urgent() { _priority = "high"; return this; }
public EmailBuilder Cc(string address) { _cc.Add(address); return this; }
public Email Build()
{
if (string.IsNullOrWhiteSpace(_from)) throw new InvalidOperationException("From is required");
if (string.IsNullOrWhiteSpace(_to)) throw new InvalidOperationException("To is required");
return new Email
{
From = _from,
To = _to,
Subject = _subject,
Body = _body,
IsHtml = _isHtml,
Priority = _priority,
Cc = [.._cc]
};
}
}
// Usage — reads like English
var email = new EmailBuilder()
.From("alice@example.com")
.To("bob@example.com")
.WithSubject("Welcome!")
.WithBody("<h1>Hello Bob!</h1>")
.AsHtml()
.Cc("manager@example.com")
.Urgent()
.Build();Test Data Builder
Builders shine in tests — create valid default objects and override only what the test needs.
C#
public class OrderBuilder
{
private int _customerId = 1;
private decimal _total = 100m;
private string _status = "pending";
private DateTime _createdAt = DateTime.UtcNow;
public OrderBuilder WithCustomer(int id) { _customerId = id; return this; }
public OrderBuilder WithTotal(decimal t) { _total = t; return this; }
public OrderBuilder WithStatus(string s) { _status = s; return this; }
public OrderBuilder CreatedAt(DateTime dt) { _createdAt = dt; return this; }
public Order Build() => new Order
{
CustomerId = _customerId,
Total = _total,
Status = _status,
CreatedAt = _createdAt,
};
}
// Tests read clearly — only the relevant property is set
[Fact]
public void LargeOrders_GetFreeShipping()
{
var order = new OrderBuilder().WithTotal(500m).Build();
Assert.True(shippingService.IsFreeShipping(order));
}
[Fact]
public void CancelledOrders_CannotBeShipped()
{
var order = new OrderBuilder().WithStatus("cancelled").Build();
Assert.False(shippingService.CanShip(order));
}Query Builder
Builders are also excellent for dynamic SQL or API queries.
C#
public class ProductQueryBuilder
{
private string? _search;
private string? _category;
private decimal? _minPrice;
private decimal? _maxPrice;
private string _sort = "name";
private bool _ascending = true;
private int _page = 1;
private int _pageSize = 20;
public ProductQueryBuilder Search(string term) { _search = term; return this; }
public ProductQueryBuilder Category(string cat) { _category = cat; return this; }
public ProductQueryBuilder PriceRange(decimal min, decimal max)
{
_minPrice = min; _maxPrice = max; return this;
}
public ProductQueryBuilder SortBy(string field, bool asc = true)
{
_sort = field; _ascending = asc; return this;
}
public ProductQueryBuilder Page(int page, int size = 20)
{
_page = page; _pageSize = size; return this;
}
public IQueryable<Product> Apply(IQueryable<Product> query)
{
if (_search is not null)
query = query.Where(p => p.Name.Contains(_search));
if (_category is not null)
query = query.Where(p => p.Category == _category);
if (_minPrice.HasValue)
query = query.Where(p => p.Price >= _minPrice.Value);
if (_maxPrice.HasValue)
query = query.Where(p => p.Price <= _maxPrice.Value);
query = _ascending
? query.OrderBy(p => EF.Property<object>(p, _sort))
: query.OrderByDescending(p => EF.Property<object>(p, _sort));
return query.Skip((_page - 1) * _pageSize).Take(_pageSize);
}
}
// Usage
var results = new ProductQueryBuilder()
.Category("Electronics")
.PriceRange(50, 500)
.SortBy("price", ascending: false)
.Page(1, 10)
.Apply(db.Products)
.ToListAsync();Key Takeaways
- Builder is best when constructing objects with many optional parameters or complex validation
- Return
thisfrom each method to enable fluent chaining - Test data builders with sensible defaults let tests set only what's relevant — tests become readable
- Validate in
Build()— catch missing required fields before creating the object initproperties (C# 9+) give you immutable objects that still work with the builder pattern
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.