The Builder Pattern: A Comprehensive Guide to Fluent Object Construction in .NET
Introduction
The Builder pattern is a creational design pattern that separates the construction of complex objects from their representation, allowing you to build objects step-by-step in a fluent, readable manner. Instead of creating objects through complex constructors with many parameters, the Builder pattern provides a clean, intuitive API for object construction.
This article explores the Builder pattern comprehensive implementation in modern .NET applications, and demonstrates how FrenchExDev.Net.Builder provides a sophisticated, async-first framework that handles complex scenarios including circular references, validation accumulation, and concurrent access patterns.
The Problem with Traditional Construction
1. Constructor Telescoping Problem
public class Order
{
public Order(int id, string customerName, List<OrderItem> items,
decimal discount, string notes, DateTime createdDate,
string shippingAddress, string billingAddress,
bool urgent, int invoiceNumber)
{
Id = id;
CustomerName = customerName;
Items = items;
Discount = discount;
Notes = notes;
CreatedDate = createdDate;
ShippingAddress = shippingAddress;
BillingAddress = billingAddress;
Urgent = urgent;
InvoiceNumber = invoiceNumber;
}
public int Id { get; }
public string CustomerName { get; }
// ... more properties
}
// Difficult to use and maintain
var order = new Order(1, "John Doe", items, 0.1m, "VIP", DateTime.Now,
"123 Main St", "123 Main St", true, 12345);
// Hard to understand what each parameter meanspublic class Order
{
public Order(int id, string customerName, List<OrderItem> items,
decimal discount, string notes, DateTime createdDate,
string shippingAddress, string billingAddress,
bool urgent, int invoiceNumber)
{
Id = id;
CustomerName = customerName;
Items = items;
Discount = discount;
Notes = notes;
CreatedDate = createdDate;
ShippingAddress = shippingAddress;
BillingAddress = billingAddress;
Urgent = urgent;
InvoiceNumber = invoiceNumber;
}
public int Id { get; }
public string CustomerName { get; }
// ... more properties
}
// Difficult to use and maintain
var order = new Order(1, "John Doe", items, 0.1m, "VIP", DateTime.Now,
"123 Main St", "123 Main St", true, 12345);
// Hard to understand what each parameter means2. Constructor Overloading Explosion
public class Order
{
// Basic constructor
public Order() { }
// Optional parameters
public Order(int id) { }
public Order(int id, string customerName) { }
public Order(int id, string customerName, List<OrderItem> items) { }
public Order(int id, string customerName, List<OrderItem> items, decimal discount) { }
// ... more overloads
}public class Order
{
// Basic constructor
public Order() { }
// Optional parameters
public Order(int id) { }
public Order(int id, string customerName) { }
public Order(int id, string customerName, List<OrderItem> items) { }
public Order(int id, string customerName, List<OrderItem> items, decimal discount) { }
// ... more overloads
}This rapidly becomes unmaintainable.
3. Immutability and Consistency
// Creating an object and then modifying it step-by-step
var order = new Order();
order.CustomerName = "John Doe"; // Mutable state
order.Items = items;
order.Discount = 0.1m;
// Object may be in an invalid state at intermediate steps// Creating an object and then modifying it step-by-step
var order = new Order();
order.CustomerName = "John Doe"; // Mutable state
order.Items = items;
order.Discount = 0.1m;
// Object may be in an invalid state at intermediate stepsIntroducing the Builder Pattern
The Builder pattern addresses these issues by:
- Separating construction from representation - The builder encapsulates the construction logic
- Fluent API - Method chaining for readable, expressive construction
- Immutability - Building and then returning a final immutable object
- Mandatory vs Optional - Clear separation of required and optional properties
- Validation - Construction logic can validate the final object
Basic Concept
public class OrderBuilder
{
private int _id;
private string _customerName = string.Empty;
private List<OrderItem> _items = new();
private decimal _discount = 0;
public OrderBuilder WithId(int id)
{
_id = id;
return this;
}
public OrderBuilder WithCustomer(string name)
{
_customerName = name;
return this;
}
public OrderBuilder AddItem(OrderItem item)
{
_items.Add(item);
return this;
}
public OrderBuilder WithDiscount(decimal discount)
{
_discount = discount;
return this;
}
public Order Build()
{
if (_id == 0) throw new InvalidOperationException("ID is required");
if (string.IsNullOrEmpty(_customerName)) throw new InvalidOperationException("Customer name is required");
return new Order(_id, _customerName, _items, _discount);
}
}
// Usage - Clear and readable
var order = new OrderBuilder()
.WithId(1)
.WithCustomer("John Doe")
.AddItem(new OrderItem { ProductId = 100, Quantity = 2 })
.AddItem(new OrderItem { ProductId = 200, Quantity = 1 })
.WithDiscount(0.1m)
.Build();public class OrderBuilder
{
private int _id;
private string _customerName = string.Empty;
private List<OrderItem> _items = new();
private decimal _discount = 0;
public OrderBuilder WithId(int id)
{
_id = id;
return this;
}
public OrderBuilder WithCustomer(string name)
{
_customerName = name;
return this;
}
public OrderBuilder AddItem(OrderItem item)
{
_items.Add(item);
return this;
}
public OrderBuilder WithDiscount(decimal discount)
{
_discount = discount;
return this;
}
public Order Build()
{
if (_id == 0) throw new InvalidOperationException("ID is required");
if (string.IsNullOrEmpty(_customerName)) throw new InvalidOperationException("Customer name is required");
return new Order(_id, _customerName, _items, _discount);
}
}
// Usage - Clear and readable
var order = new OrderBuilder()
.WithId(1)
.WithCustomer("John Doe")
.AddItem(new OrderItem { ProductId = 100, Quantity = 2 })
.AddItem(new OrderItem { ProductId = 200, Quantity = 1 })
.WithDiscount(0.1m)
.Build();FrenchExDev.Net.Builder: Async-First, Production-Grade Implementation
FrenchExDev.Net.Builder provides a sophisticated framework designed specifically for enterprise scenarios, featuring:
- Async-first design - All building is async by default with
async/awaitsupport - Reference-based construction - Handles circular references and complex graphs using
Reference<T> - Validation accumulation - Collects all validation errors before failing, not fail-fast
- Concurrent safety - Single-flight execution with semaphore locking
- Type-safe errors - Generic
AbstractBuilder<T, TException>returns Result<T, TException> with typed exceptions - Graph support - Build complex object hierarchies with parent-child relationships
Core Abstractions
namespace FrenchExDev.Net.Builder;
// Core marker interface for all builders
public interface IBuilder
{
}
// Reference to a lazy-loaded or to-be-built object
public sealed record Reference<TClass> where TClass : notnull
{
public bool IsResolved { get; }
public Reference<TClass> Resolve(TClass referenced);
public bool TryResolved(out TClass referenced);
public TClass Resolved();
}
// Accumulates validation errors without throwing
public sealed class ValidationResult
{
public bool IsSuccess { get; }
public void AddError(MemberName memberName, Exception exception);
public void AddErrors(MemberName memberName, IEnumerable<Exception> exceptions);
public DataAnnotationsValidationResult ToDataAnnotationsValidationResult();
}
// Core builder interface - returns Reference<T> for lazy/circular handling
public interface IBuilder<TClass> : IBuilder where TClass : notnull
{
Task<Result<Reference<TClass>>> BuildAsync(
VisitedObjects? visitedObjects = null,
CancellationToken cancellationToken = default);
Reference<TClass> Reference();
}
// Generic builder with custom exception type
public interface IBuilder<TResult, TError> : IBuilder
where TResult : notnull
where TError : Exception
{
Task<Result<TResult, TError>> BuildAsync(CancellationToken cancellationToken = default);
Reference<Result<TResult, TError>> Reference();
}namespace FrenchExDev.Net.Builder;
// Core marker interface for all builders
public interface IBuilder
{
}
// Reference to a lazy-loaded or to-be-built object
public sealed record Reference<TClass> where TClass : notnull
{
public bool IsResolved { get; }
public Reference<TClass> Resolve(TClass referenced);
public bool TryResolved(out TClass referenced);
public TClass Resolved();
}
// Accumulates validation errors without throwing
public sealed class ValidationResult
{
public bool IsSuccess { get; }
public void AddError(MemberName memberName, Exception exception);
public void AddErrors(MemberName memberName, IEnumerable<Exception> exceptions);
public DataAnnotationsValidationResult ToDataAnnotationsValidationResult();
}
// Core builder interface - returns Reference<T> for lazy/circular handling
public interface IBuilder<TClass> : IBuilder where TClass : notnull
{
Task<Result<Reference<TClass>>> BuildAsync(
VisitedObjects? visitedObjects = null,
CancellationToken cancellationToken = default);
Reference<TClass> Reference();
}
// Generic builder with custom exception type
public interface IBuilder<TResult, TError> : IBuilder
where TResult : notnull
where TError : Exception
{
Task<Result<TResult, TError>> BuildAsync(CancellationToken cancellationToken = default);
Reference<Result<TResult, TError>> Reference();
}The Build Flow
Every builder follows this internal flow:
- Validation -
ValidateAsync()collects all errors intoValidationResult - Check Errors - If errors exist, return
Result.Failure() - Instantiate -
InstantiateAsync()creates the object and resolves the reference - Cache - Result is cached using semaphore-based single-flight pattern
- Return - Subsequent calls return cached reference immediately
Implementation Patterns
Pattern 1: Basic AbstractBuilder
The simplest pattern: inherit from AbstractBuilder<T> and implement three methods.
public class UserBuilder : AbstractBuilder<User>
{
private string _email = string.Empty;
private string _name = string.Empty;
public UserBuilder WithEmail(string email)
{
_email = email;
return this;
}
public UserBuilder WithName(string name)
{
_name = name;
return this;
}
// Validate before building
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_email))
result.AddError(
new MemberName(nameof(_email), typeof(UserBuilder)),
new InvalidOperationException("Email is required"));
if (string.IsNullOrWhiteSpace(_name))
result.AddError(
new MemberName(nameof(_name), typeof(UserBuilder)),
new InvalidOperationException("Name is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// Create the instance
protected override Task<Result<Reference<User>>> InstantiateAsync(
Reference<User> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var user = new User { Email = _email, Name = _name };
reference.Resolve(user);
return Task.FromResult(Result<Reference<User>>.Success(reference));
}
// Convert validation errors to exception
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
// Usage - BuildAsync() returns Result<Reference<User>>
var userResult = await new UserBuilder()
.WithEmail("john@example.com")
.WithName("John Doe")
.BuildAsync();
if (userResult.IsSuccess)
{
var user = userResult.ValueOrThrow().Resolved();
Console.WriteLine($"Built user: {user.Name}");
}
else
{
Console.WriteLine($"Build failed: {userResult.ValidationResult?.ErrorMessage}");
}public class UserBuilder : AbstractBuilder<User>
{
private string _email = string.Empty;
private string _name = string.Empty;
public UserBuilder WithEmail(string email)
{
_email = email;
return this;
}
public UserBuilder WithName(string name)
{
_name = name;
return this;
}
// Validate before building
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_email))
result.AddError(
new MemberName(nameof(_email), typeof(UserBuilder)),
new InvalidOperationException("Email is required"));
if (string.IsNullOrWhiteSpace(_name))
result.AddError(
new MemberName(nameof(_name), typeof(UserBuilder)),
new InvalidOperationException("Name is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// Create the instance
protected override Task<Result<Reference<User>>> InstantiateAsync(
Reference<User> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var user = new User { Email = _email, Name = _name };
reference.Resolve(user);
return Task.FromResult(Result<Reference<User>>.Success(reference));
}
// Convert validation errors to exception
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
// Usage - BuildAsync() returns Result<Reference<User>>
var userResult = await new UserBuilder()
.WithEmail("john@example.com")
.WithName("John Doe")
.BuildAsync();
if (userResult.IsSuccess)
{
var user = userResult.ValueOrThrow().Resolved();
Console.WriteLine($"Built user: {user.Name}");
}
else
{
Console.WriteLine($"Build failed: {userResult.ValidationResult?.ErrorMessage}");
}Key Points:
ValidateAsync()accumulates ALL errors before returning (not fail-fast)InstantiateAsync()resolves the reference before returning- Automatic caching - second
BuildAsync()call returns cached reference - Thread-safe with semaphore-based single-flight pattern
Pattern 2: Generic Builder with Custom Exception
For scenarios where you want strongly-typed exceptions instead of generic InvalidOperationException:
public class OrderCreationException : Exception
{
public OrderCreationException(string message) : base(message) { }
}
public class OrderBuilder : AbstractBuilder<Order, OrderCreationException>
{
private int _id;
private List<OrderItem> _items = new();
private decimal _discount = 0;
public OrderBuilder WithId(int id)
{
_id = id;
return this;
}
public OrderBuilder AddItem(OrderItem item)
{
_items.Add(item);
return this;
}
public OrderBuilder WithDiscount(decimal discount)
{
_discount = discount;
return this;
}
// The base AbstractBuilder<T, TException> handles BuildAsync for you.
// Implement validation, instantiation, and typed exception conversion.
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (_id <= 0)
result.AddError(
new MemberName(nameof(_id), typeof(OrderBuilder)),
new InvalidOperationException("ID must be positive"));
if (!_items.Any())
result.AddError(
new MemberName(nameof(_items), typeof(OrderBuilder)),
new InvalidOperationException("Order must have at least one item"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Order, OrderCreationException>> InstantiateAsync(CancellationToken ct)
{
// Simulate async work
await Task.Delay(10, ct).ConfigureAwait(false);
var order = new Order { Id = _id, Items = _items, Discount = _discount };
return Result<Order, OrderCreationException>.Success(order);
}
protected override OrderCreationException TypedBuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new OrderCreationException(vr.ErrorMessage);
}
}
// Usage - Returns Result<Order, OrderCreationException>
var orderResult = await new OrderBuilder()
.WithId(1)
.AddItem(new OrderItem { ProductId = 100, Quantity = 2 })
.WithDiscount(0.1m)
.BuildAsync();
if (orderResult.IsSuccess)
{
Console.WriteLine($"Order {orderResult.Value.Id} created");
}
else
{
Console.WriteLine($"OrderCreationException: {orderResult.Error!.Message}");
}public class OrderCreationException : Exception
{
public OrderCreationException(string message) : base(message) { }
}
public class OrderBuilder : AbstractBuilder<Order, OrderCreationException>
{
private int _id;
private List<OrderItem> _items = new();
private decimal _discount = 0;
public OrderBuilder WithId(int id)
{
_id = id;
return this;
}
public OrderBuilder AddItem(OrderItem item)
{
_items.Add(item);
return this;
}
public OrderBuilder WithDiscount(decimal discount)
{
_discount = discount;
return this;
}
// The base AbstractBuilder<T, TException> handles BuildAsync for you.
// Implement validation, instantiation, and typed exception conversion.
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (_id <= 0)
result.AddError(
new MemberName(nameof(_id), typeof(OrderBuilder)),
new InvalidOperationException("ID must be positive"));
if (!_items.Any())
result.AddError(
new MemberName(nameof(_items), typeof(OrderBuilder)),
new InvalidOperationException("Order must have at least one item"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Order, OrderCreationException>> InstantiateAsync(CancellationToken ct)
{
// Simulate async work
await Task.Delay(10, ct).ConfigureAwait(false);
var order = new Order { Id = _id, Items = _items, Discount = _discount };
return Result<Order, OrderCreationException>.Success(order);
}
protected override OrderCreationException TypedBuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new OrderCreationException(vr.ErrorMessage);
}
}
// Usage - Returns Result<Order, OrderCreationException>
var orderResult = await new OrderBuilder()
.WithId(1)
.AddItem(new OrderItem { ProductId = 100, Quantity = 2 })
.WithDiscount(0.1m)
.BuildAsync();
if (orderResult.IsSuccess)
{
Console.WriteLine($"Order {orderResult.Value.Id} created");
}
else
{
Console.WriteLine($"OrderCreationException: {orderResult.Error!.Message}");
}Pattern 3: Building Object Graphs with Parent-Child Relationships
Complex hierarchies with circular references handled transparently via VisitedObjects:
public class Person
{
public string Name { get; set; } = "";
public List<Child> Children { get; set; } = new();
}
public class Child
{
public string Name { get; set; } = "";
public Person? Parent { get; set; }
}
public class PersonBuilder : AbstractBuilder<Person>
{
private string _name = string.Empty;
private readonly List<ChildBuilder> _childBuilders = new();
public PersonBuilder WithName(string name)
{
_name = name;
return this;
}
public PersonBuilder AddChild(string childName)
{
_childBuilders.Add(new ChildBuilder(childName, this));
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_name))
result.AddError(
new MemberName(nameof(_name), typeof(PersonBuilder)),
new InvalidOperationException("Name is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<Person>>> InstantiateAsync(
Reference<Person> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var person = new Person { Name = _name };
reference.Resolve(person);
// Build children, reusing visited for circular reference handling
foreach (var childBuilder in _childBuilders)
{
var childResult = await childBuilder.BuildAsync(visitedObjects, ct);
if (!childResult.IsSuccess)
return childResult;
var child = childResult.ValueOrThrow().Resolved();
child.Parent = person; // Back-reference
person.Children.Add(child);
}
return Result<Reference<Person>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
public class ChildBuilder : AbstractBuilder<Child>
{
private string _name = string.Empty;
private readonly PersonBuilder _parentBuilder;
public ChildBuilder(string name, PersonBuilder parentBuilder)
{
_name = name;
_parentBuilder = parentBuilder;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
=> Task.FromResult(Result<ValidationResult>.Success(new ValidationResult()));
protected override Task<Result<Reference<Child>>> InstantiateAsync(
Reference<Child> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var child = new Child { Name = _name };
reference.Resolve(child);
return Task.FromResult(Result<Reference<Child>>.Success(reference));
}
protected override Exception BuildException(Result<ValidationResult> vr)
=> new InvalidOperationException("Child build failed");
}
// Usage - Builds entire graph with parent-child relationships
var personBuilder = new PersonBuilder()
.WithName("Alice")
.AddChild("Bob")
.AddChild("Claire");
var result = await personBuilder.BuildAsync();
if (result.IsSuccess)
{
var person = result.ValueOrThrow().Resolved();
Console.WriteLine($"{person.Name} has {person.Children.Count} children");
foreach (var child in person.Children)
{
Console.WriteLine($" - {child.Name} (parent: {child.Parent?.Name})");
}
}public class Person
{
public string Name { get; set; } = "";
public List<Child> Children { get; set; } = new();
}
public class Child
{
public string Name { get; set; } = "";
public Person? Parent { get; set; }
}
public class PersonBuilder : AbstractBuilder<Person>
{
private string _name = string.Empty;
private readonly List<ChildBuilder> _childBuilders = new();
public PersonBuilder WithName(string name)
{
_name = name;
return this;
}
public PersonBuilder AddChild(string childName)
{
_childBuilders.Add(new ChildBuilder(childName, this));
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_name))
result.AddError(
new MemberName(nameof(_name), typeof(PersonBuilder)),
new InvalidOperationException("Name is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<Person>>> InstantiateAsync(
Reference<Person> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var person = new Person { Name = _name };
reference.Resolve(person);
// Build children, reusing visited for circular reference handling
foreach (var childBuilder in _childBuilders)
{
var childResult = await childBuilder.BuildAsync(visitedObjects, ct);
if (!childResult.IsSuccess)
return childResult;
var child = childResult.ValueOrThrow().Resolved();
child.Parent = person; // Back-reference
person.Children.Add(child);
}
return Result<Reference<Person>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
public class ChildBuilder : AbstractBuilder<Child>
{
private string _name = string.Empty;
private readonly PersonBuilder _parentBuilder;
public ChildBuilder(string name, PersonBuilder parentBuilder)
{
_name = name;
_parentBuilder = parentBuilder;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
=> Task.FromResult(Result<ValidationResult>.Success(new ValidationResult()));
protected override Task<Result<Reference<Child>>> InstantiateAsync(
Reference<Child> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var child = new Child { Name = _name };
reference.Resolve(child);
return Task.FromResult(Result<Reference<Child>>.Success(reference));
}
protected override Exception BuildException(Result<ValidationResult> vr)
=> new InvalidOperationException("Child build failed");
}
// Usage - Builds entire graph with parent-child relationships
var personBuilder = new PersonBuilder()
.WithName("Alice")
.AddChild("Bob")
.AddChild("Claire");
var result = await personBuilder.BuildAsync();
if (result.IsSuccess)
{
var person = result.ValueOrThrow().Resolved();
Console.WriteLine($"{person.Name} has {person.Children.Count} children");
foreach (var child in person.Children)
{
Console.WriteLine($" - {child.Name} (parent: {child.Parent?.Name})");
}
}Key Points:
VisitedObjectstracks already-built objects to prevent infinite recursion- Recursive calls reuse the same visited dictionary
- Parent-child relationships automatically connected
- Thread-safe graph construction
Pattern 4: Concurrent Access and Caching
Multiple concurrent calls automatically coordinated via semaphore:
public class ConfigurationBuilder : AbstractBuilder<Configuration>
{
private readonly Dictionary<string, string> _settings = new();
public ConfigurationBuilder Set(string key, string value)
{
_settings[key] = value;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
=> Task.FromResult(Result<ValidationResult>.Success(new ValidationResult()));
protected override async Task<Result<Reference<Configuration>>> InstantiateAsync(
Reference<Configuration> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
// Simulate expensive async initialization (I/O, computation, etc.)
await Task.Delay(100, ct).ConfigureAwait(false);
var config = new Configuration(_settings);
reference.Resolve(config);
return Result<Reference<Configuration>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> vr)
=> new InvalidOperationException("Configuration build failed");
}
// Usage - Multiple concurrent calls, only one instantiation
var builder = new ConfigurationBuilder()
.Set("Database", "ProductionDB")
.Set("Timeout", "30000");
// All these calls will wait their turn and share the same result
var task1 = builder.BuildAsync();
var task2 = builder.BuildAsync();
var task3 = builder.BuildAsync();
var results = await Task.WhenAll(task1, task2, task3);
// All results point to the same Reference<Configuration>
Assert.True(results.All(r => ReferenceEquals(
results[0].ValueOrThrow(),
r.ValueOrThrow())));public class ConfigurationBuilder : AbstractBuilder<Configuration>
{
private readonly Dictionary<string, string> _settings = new();
public ConfigurationBuilder Set(string key, string value)
{
_settings[key] = value;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
=> Task.FromResult(Result<ValidationResult>.Success(new ValidationResult()));
protected override async Task<Result<Reference<Configuration>>> InstantiateAsync(
Reference<Configuration> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
// Simulate expensive async initialization (I/O, computation, etc.)
await Task.Delay(100, ct).ConfigureAwait(false);
var config = new Configuration(_settings);
reference.Resolve(config);
return Result<Reference<Configuration>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> vr)
=> new InvalidOperationException("Configuration build failed");
}
// Usage - Multiple concurrent calls, only one instantiation
var builder = new ConfigurationBuilder()
.Set("Database", "ProductionDB")
.Set("Timeout", "30000");
// All these calls will wait their turn and share the same result
var task1 = builder.BuildAsync();
var task2 = builder.BuildAsync();
var task3 = builder.BuildAsync();
var results = await Task.WhenAll(task1, task2, task3);
// All results point to the same Reference<Configuration>
Assert.True(results.All(r => ReferenceEquals(
results[0].ValueOrThrow(),
r.ValueOrThrow())));Pattern 5: Validation Error Accumulation
Unlike fail-fast builders, FrenchExDev.Net.Builder accumulates all errors:
public class StrictUserBuilder : AbstractBuilder<User>
{
private string _email = string.Empty;
private string _name = string.Empty;
private int _age = 0;
private string _phone = string.Empty;
public StrictUserBuilder WithEmail(string email)
{
_email = email;
return this;
}
public StrictUserBuilder WithName(string name)
{
_name = name;
return this;
}
public StrictUserBuilder WithAge(int age)
{
_age = age;
return this;
}
public StrictUserBuilder WithPhone(string phone)
{
_phone = phone;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// All validations run - none are skipped
if (string.IsNullOrWhiteSpace(_email))
result.AddError(
new MemberName(nameof(_email), typeof(StrictUserBuilder)),
new InvalidOperationException("Email is required"));
else if (!_email.Contains("@"))
result.AddError(
new MemberName(nameof(_email), typeof(StrictUserBuilder)),
new InvalidOperationException("Email format is invalid"));
if (string.IsNullOrWhiteSpace(_name))
result.AddError(
new MemberName(nameof(_name), typeof(StrictUserBuilder)),
new InvalidOperationException("Name is required"));
if (_age < 18)
result.AddError(
new MemberName(nameof(_age), typeof(StrictUserBuilder)),
new InvalidOperationException("Age must be 18 or older"));
if (string.IsNullOrWhiteSpace(_phone))
result.AddError(
new MemberName(nameof(_phone), typeof(StrictUserBuilder)),
new InvalidOperationException("Phone is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override Task<Result<Reference<User>>> InstantiateAsync(
Reference<User> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var user = new User { Email = _email, Name = _name, Age = _age, Phone = _phone };
reference.Resolve(user);
return Task.FromResult(Result<Reference<User>>.Success(reference));
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
// Usage - All error messages returned together
var result = await new StrictUserBuilder()
.WithEmail("invalid") // Invalid format
.WithName("") // Empty
.WithAge(16) // Too young
// .WithPhone() missing
.BuildAsync();
if (!result.IsSuccess)
{
var errors = result.ValidationResult!.ErrorMessage;
// Output: "Email format is invalid; Name is required; Age must be 18 or older; Phone is required"
Console.WriteLine($"All validation errors: {errors}");
}public class StrictUserBuilder : AbstractBuilder<User>
{
private string _email = string.Empty;
private string _name = string.Empty;
private int _age = 0;
private string _phone = string.Empty;
public StrictUserBuilder WithEmail(string email)
{
_email = email;
return this;
}
public StrictUserBuilder WithName(string name)
{
_name = name;
return this;
}
public StrictUserBuilder WithAge(int age)
{
_age = age;
return this;
}
public StrictUserBuilder WithPhone(string phone)
{
_phone = phone;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// All validations run - none are skipped
if (string.IsNullOrWhiteSpace(_email))
result.AddError(
new MemberName(nameof(_email), typeof(StrictUserBuilder)),
new InvalidOperationException("Email is required"));
else if (!_email.Contains("@"))
result.AddError(
new MemberName(nameof(_email), typeof(StrictUserBuilder)),
new InvalidOperationException("Email format is invalid"));
if (string.IsNullOrWhiteSpace(_name))
result.AddError(
new MemberName(nameof(_name), typeof(StrictUserBuilder)),
new InvalidOperationException("Name is required"));
if (_age < 18)
result.AddError(
new MemberName(nameof(_age), typeof(StrictUserBuilder)),
new InvalidOperationException("Age must be 18 or older"));
if (string.IsNullOrWhiteSpace(_phone))
result.AddError(
new MemberName(nameof(_phone), typeof(StrictUserBuilder)),
new InvalidOperationException("Phone is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override Task<Result<Reference<User>>> InstantiateAsync(
Reference<User> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var user = new User { Email = _email, Name = _name, Age = _age, Phone = _phone };
reference.Resolve(user);
return Task.FromResult(Result<Reference<User>>.Success(reference));
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
// Usage - All error messages returned together
var result = await new StrictUserBuilder()
.WithEmail("invalid") // Invalid format
.WithName("") // Empty
.WithAge(16) // Too young
// .WithPhone() missing
.BuildAsync();
if (!result.IsSuccess)
{
var errors = result.ValidationResult!.ErrorMessage;
// Output: "Email format is invalid; Name is required; Age must be 18 or older; Phone is required"
Console.WriteLine($"All validation errors: {errors}");
}Best Practices with FrenchExDev.Net.Builder
1. Implement Three Core Methods
Every builder must implement these abstract methods:
// ✓ Good: Implement all three required methods
public class MyBuilder : AbstractBuilder<MyType>
{
// 1. Validate - collect errors, don't throw
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// Add errors for each invalid condition
if (invalid)
result.AddError(new MemberName(nameof(Property), typeof(MyBuilder)),
new InvalidOperationException("Property is invalid"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// 2. Instantiate - create and resolve reference
protected override Task<Result<Reference<MyType>>> InstantiateAsync(
Reference<MyType> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var instance = new MyType { /*...*/ };
reference.Resolve(instance);
return Task.FromResult(Result<Reference<MyType>>.Success(reference));
}
// 3. BuildException - convert validation result to exception
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
// ✗ Avoid: Throwing directly from builder properties
public OrderBuilder WithQuantity(int quantity)
{
if (quantity <= 0)
throw new InvalidOperationException("Quantity must be positive");
_quantity = quantity;
return this;
}// ✓ Good: Implement all three required methods
public class MyBuilder : AbstractBuilder<MyType>
{
// 1. Validate - collect errors, don't throw
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// Add errors for each invalid condition
if (invalid)
result.AddError(new MemberName(nameof(Property), typeof(MyBuilder)),
new InvalidOperationException("Property is invalid"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// 2. Instantiate - create and resolve reference
protected override Task<Result<Reference<MyType>>> InstantiateAsync(
Reference<MyType> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var instance = new MyType { /*...*/ };
reference.Resolve(instance);
return Task.FromResult(Result<Reference<MyType>>.Success(reference));
}
// 3. BuildException - convert validation result to exception
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException(vr.ErrorMessage);
}
}
// ✗ Avoid: Throwing directly from builder properties
public OrderBuilder WithQuantity(int quantity)
{
if (quantity <= 0)
throw new InvalidOperationException("Quantity must be positive");
_quantity = quantity;
return this;
}2. Accumulate Validation Errors, Not Fail-Fast
// ✓ Good: Accumulate ALL errors before reporting
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrEmpty(Email))
result.AddError(new MemberName(nameof(Email), typeof(UserBuilder)),
new InvalidOperationException("Email is required"));
if (Age < 18)
result.AddError(new MemberName(nameof(Age), typeof(UserBuilder)),
new InvalidOperationException("Age must be 18+"));
if (string.IsNullOrEmpty(Phone))
result.AddError(new MemberName(nameof(Phone), typeof(UserBuilder)),
new InvalidOperationException("Phone is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// ✗ Avoid: Early return after first error
protected override async Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrEmpty(Email))
{
result.AddError(new MemberName(nameof(Email), typeof(UserBuilder)),
new InvalidOperationException("Email is required"));
return Task.FromResult(Result<ValidationResult>.Success(result)); // Only one error
}
// ...remaining validations skipped
}// ✓ Good: Accumulate ALL errors before reporting
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrEmpty(Email))
result.AddError(new MemberName(nameof(Email), typeof(UserBuilder)),
new InvalidOperationException("Email is required"));
if (Age < 18)
result.AddError(new MemberName(nameof(Age), typeof(UserBuilder)),
new InvalidOperationException("Age must be 18+"));
if (string.IsNullOrEmpty(Phone))
result.AddError(new MemberName(nameof(Phone), typeof(UserBuilder)),
new InvalidOperationException("Phone is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// ✗ Avoid: Early return after first error
protected override async Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrEmpty(Email))
{
result.AddError(new MemberName(nameof(Email), typeof(UserBuilder)),
new InvalidOperationException("Email is required"));
return Task.FromResult(Result<ValidationResult>.Success(result)); // Only one error
}
// ...remaining validations skipped
}3. Use Reference for Circular References
// ✓ Good: Handle parent-child circular references
protected override async Task<Result<Reference<Person>>> InstantiateAsync(
Reference<Person> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var person = new Person { Name = _name };
reference.Resolve(person); // Resolve early, before building children
foreach (var childBuilder in _childBuilders)
{
var childResult = await childBuilder.BuildAsync(visitedObjects, ct);
if (!childResult.IsSuccess)
return childResult;
var child = childResult.ValueOrThrow().Resolved();
child.Parent = person; // Back-reference now valid
person.Children.Add(child);
}
return Result<Reference<Person>>.Success(reference);
}// ✓ Good: Handle parent-child circular references
protected override async Task<Result<Reference<Person>>> InstantiateAsync(
Reference<Person> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var person = new Person { Name = _name };
reference.Resolve(person); // Resolve early, before building children
foreach (var childBuilder in _childBuilders)
{
var childResult = await childBuilder.BuildAsync(visitedObjects, ct);
if (!childResult.IsSuccess)
return childResult;
var child = childResult.ValueOrThrow().Resolved();
child.Parent = person; // Back-reference now valid
person.Children.Add(child);
}
return Result<Reference<Person>>.Success(reference);
}4. Always Use CancellationToken
// ✓ Good: Pass cancellation token through the chain
protected override async Task<Result<Reference<Configuration>>> InstantiateAsync(
Reference<Configuration> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
await Task.Delay(100, ct).ConfigureAwait(false); // Respect cancellation
var config = new Configuration();
reference.Resolve(config);
return Result<Reference<Configuration>>.Success(reference);
}
// Usage
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result = await builder.BuildAsync(cancellationToken: cts.Token);// ✓ Good: Pass cancellation token through the chain
protected override async Task<Result<Reference<Configuration>>> InstantiateAsync(
Reference<Configuration> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
await Task.Delay(100, ct).ConfigureAwait(false); // Respect cancellation
var config = new Configuration();
reference.Resolve(config);
return Result<Reference<Configuration>>.Success(reference);
}
// Usage
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var result = await builder.BuildAsync(cancellationToken: cts.Token);5. Leverage Automatic Caching
// ✓ Good: Call BuildAsync multiple times - reuses cached reference
var builder = new ConfigurationBuilder().Set("key", "value");
var result1 = await builder.BuildAsync(); // Calls InstantiateAsync
var result2 = await builder.BuildAsync(); // Returns cached reference (no InstantiateAsync call)
var result3 = await builder.BuildAsync(); // Returns cached reference
// All point to the same Reference<T> instance
Assert.Same(result1.ValueOrThrow(), result2.ValueOrThrow());// ✓ Good: Call BuildAsync multiple times - reuses cached reference
var builder = new ConfigurationBuilder().Set("key", "value");
var result1 = await builder.BuildAsync(); // Calls InstantiateAsync
var result2 = await builder.BuildAsync(); // Returns cached reference (no InstantiateAsync call)
var result3 = await builder.BuildAsync(); // Returns cached reference
// All point to the same Reference<T> instance
Assert.Same(result1.ValueOrThrow(), result2.ValueOrThrow());6. Handle Generic Exception Type When Appropriate
// ✓ Good: Use AbstractBuilder<T, TException> for domain exceptions
public class PaymentBuilder : AbstractBuilder<Payment, PaymentException>
{
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// Add validation errors here, e.g.:
// if (amount <= 0) result.AddError(...);
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Payment, PaymentException>> InstantiateAsync(CancellationToken ct)
{
// Simulate async initialization (e.g., I/O, external service)
await Task.Delay(10, ct).ConfigureAwait(false);
var payment = new Payment();
return Result<Payment, PaymentException>.Success(payment);
}
protected override PaymentException TypedBuildException(Result<ValidationResult> vr)
=> new PaymentException(vr.ValueOrThrow()
.ToDataAnnotationsValidationResult().ErrorMessage);
}
// Usage - Result carries the specific exception type
var result = await builder.BuildAsync();
if (!result.IsSuccess)
{
var paymentException = result.Error; // PaymentException, not generic Exception
}// ✓ Good: Use AbstractBuilder<T, TException> for domain exceptions
public class PaymentBuilder : AbstractBuilder<Payment, PaymentException>
{
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// Add validation errors here, e.g.:
// if (amount <= 0) result.AddError(...);
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Payment, PaymentException>> InstantiateAsync(CancellationToken ct)
{
// Simulate async initialization (e.g., I/O, external service)
await Task.Delay(10, ct).ConfigureAwait(false);
var payment = new Payment();
return Result<Payment, PaymentException>.Success(payment);
}
protected override PaymentException TypedBuildException(Result<ValidationResult> vr)
=> new PaymentException(vr.ValueOrThrow()
.ToDataAnnotationsValidationResult().ErrorMessage);
}
// Usage - Result carries the specific exception type
var result = await builder.BuildAsync();
if (!result.IsSuccess)
{
var paymentException = result.Error; // PaymentException, not generic Exception
}Real-World Examples
Example 1: Asynchronous Database Configuration Builder
public class DatabaseConnectionBuilder : AbstractBuilder<DatabaseConnection>
{
private string _host = string.Empty;
private int _port = 5432;
private string _database = string.Empty;
private string _username = string.Empty;
private string _password = string.Empty;
private bool _verifyConnection = false;
public DatabaseConnectionBuilder WithHost(string host)
{
_host = host;
return this;
}
public DatabaseConnectionBuilder WithPort(int port)
{
_port = port;
return this;
}
public DatabaseConnectionBuilder WithDatabase(string database)
{
_database = database;
return this;
}
public DatabaseConnectionBuilder WithCredentials(string username, string password)
{
_username = username;
_password = password;
return this;
}
public DatabaseConnectionBuilder VerifyConnection()
{
_verifyConnection = true;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_host))
result.AddError(new MemberName(nameof(_host), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Host is required"));
if (_port <= 0 || _port > 65535)
result.AddError(new MemberName(nameof(_port), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Port must be between 1 and 65535"));
if (string.IsNullOrWhiteSpace(_database))
result.AddError(new MemberName(nameof(_database), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Database name is required"));
if (string.IsNullOrWhiteSpace(_username))
result.AddError(new MemberName(nameof(_username), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Username is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<DatabaseConnection>>> InstantiateAsync(
Reference<DatabaseConnection> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var connection = new DatabaseConnection
{
Host = _host,
Port = _port,
Database = _database,
Username = _username,
Password = _password
};
if (_verifyConnection)
{
var connectionString = $"Server={_host};Port={_port};Database={_database};User Id={_username};Password={_password}";
// Simulate async verification (real implementation would test the connection)
await Task.Delay(100, ct).ConfigureAwait(false);
// In real code, this would actually attempt to connect
connection.IsVerified = true;
}
reference.Resolve(connection);
return Result<Reference<DatabaseConnection>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException($"DatabaseConnection build failed: {vr.ErrorMessage}");
}
}
// Usage
var dbResult = await new DatabaseConnectionBuilder()
.WithHost("db.example.com")
.WithPort(5432)
.WithDatabase("ProductionDb")
.WithCredentials("admin", "SecurePassword123")
.VerifyConnection()
.BuildAsync();
if (dbResult.IsSuccess)
{
var connection = dbResult.ValueOrThrow().Resolved();
Console.WriteLine($"Connected to {connection.Database} at {connection.Host}:{connection.Port}");
Console.WriteLine($"Verified: {connection.IsVerified}");
}
else
{
Console.WriteLine($"Connection build failed: {dbResult.ValidationResult?.ErrorMessage}");
}public class DatabaseConnectionBuilder : AbstractBuilder<DatabaseConnection>
{
private string _host = string.Empty;
private int _port = 5432;
private string _database = string.Empty;
private string _username = string.Empty;
private string _password = string.Empty;
private bool _verifyConnection = false;
public DatabaseConnectionBuilder WithHost(string host)
{
_host = host;
return this;
}
public DatabaseConnectionBuilder WithPort(int port)
{
_port = port;
return this;
}
public DatabaseConnectionBuilder WithDatabase(string database)
{
_database = database;
return this;
}
public DatabaseConnectionBuilder WithCredentials(string username, string password)
{
_username = username;
_password = password;
return this;
}
public DatabaseConnectionBuilder VerifyConnection()
{
_verifyConnection = true;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_host))
result.AddError(new MemberName(nameof(_host), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Host is required"));
if (_port <= 0 || _port > 65535)
result.AddError(new MemberName(nameof(_port), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Port must be between 1 and 65535"));
if (string.IsNullOrWhiteSpace(_database))
result.AddError(new MemberName(nameof(_database), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Database name is required"));
if (string.IsNullOrWhiteSpace(_username))
result.AddError(new MemberName(nameof(_username), typeof(DatabaseConnectionBuilder)),
new InvalidOperationException("Username is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<DatabaseConnection>>> InstantiateAsync(
Reference<DatabaseConnection> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
var connection = new DatabaseConnection
{
Host = _host,
Port = _port,
Database = _database,
Username = _username,
Password = _password
};
if (_verifyConnection)
{
var connectionString = $"Server={_host};Port={_port};Database={_database};User Id={_username};Password={_password}";
// Simulate async verification (real implementation would test the connection)
await Task.Delay(100, ct).ConfigureAwait(false);
// In real code, this would actually attempt to connect
connection.IsVerified = true;
}
reference.Resolve(connection);
return Result<Reference<DatabaseConnection>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException($"DatabaseConnection build failed: {vr.ErrorMessage}");
}
}
// Usage
var dbResult = await new DatabaseConnectionBuilder()
.WithHost("db.example.com")
.WithPort(5432)
.WithDatabase("ProductionDb")
.WithCredentials("admin", "SecurePassword123")
.VerifyConnection()
.BuildAsync();
if (dbResult.IsSuccess)
{
var connection = dbResult.ValueOrThrow().Resolved();
Console.WriteLine($"Connected to {connection.Database} at {connection.Host}:{connection.Port}");
Console.WriteLine($"Verified: {connection.IsVerified}");
}
else
{
Console.WriteLine($"Connection build failed: {dbResult.ValidationResult?.ErrorMessage}");
}Example 2: Dependency Injection Container Builder
public class ServiceContainerBuilder : AbstractBuilder<ServiceContainer>
{
private readonly List<ServiceDescriptor> _services = new();
private string _name = string.Empty;
public ServiceContainerBuilder Named(string name)
{
_name = name;
return this;
}
public ServiceContainerBuilder AddService<TInterface, TImplementation>(ServiceLifetime lifetime = ServiceLifetime.Transient)
where TInterface : class
where TImplementation : class, TInterface
{
_services.Add(new ServiceDescriptor(typeof(TInterface), typeof(TImplementation), lifetime));
return this;
}
public ServiceContainerBuilder AddSingleton<TInterface>(TInterface instance) where TInterface : class
{
_services.Add(ServiceDescriptor.Singleton(typeof(TInterface), instance));
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_name))
result.AddError(new MemberName(nameof(_name), typeof(ServiceContainerBuilder)),
new InvalidOperationException("Container name is required"));
if (!_services.Any())
result.AddError(new MemberName(nameof(_services), typeof(ServiceContainerBuilder)),
new InvalidOperationException("At least one service must be registered"));
// Check for duplicate service registrations
var duplicates = _services
.GroupBy(s => s.ServiceType)
.Where(g => g.Count() > 1);
foreach (var dup in duplicates)
result.AddError(new MemberName(nameof(_services), typeof(ServiceContainerBuilder)),
new InvalidOperationException($"Service {dup.Key.Name} is registered multiple times"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<ServiceContainer>>> InstantiateAsync(
Reference<ServiceContainer> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
// Simulate async container initialization
await Task.Delay(50, ct).ConfigureAwait(false);
var container = new ServiceContainer(_name, _services);
reference.Resolve(container);
return Result<Reference<ServiceContainer>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException($"ServiceContainer build failed: {vr.ErrorMessage}");
}
}
// Usage
var containerResult = await new ServiceContainerBuilder()
.Named("WebServices")
.AddService<IUserRepository, UserRepository>(ServiceLifetime.Scoped)
.AddService<IOrderService, OrderService>(ServiceLifetime.Scoped)
.AddSingleton<ILogger>(new ConsoleLogger())
.BuildAsync();
if (containerResult.IsSuccess)
{
var container = containerResult.ValueOrThrow().Resolved();
Console.WriteLine($"Container '{container.Name}' initialized with {container.ServiceCount} services");
}public class ServiceContainerBuilder : AbstractBuilder<ServiceContainer>
{
private readonly List<ServiceDescriptor> _services = new();
private string _name = string.Empty;
public ServiceContainerBuilder Named(string name)
{
_name = name;
return this;
}
public ServiceContainerBuilder AddService<TInterface, TImplementation>(ServiceLifetime lifetime = ServiceLifetime.Transient)
where TInterface : class
where TImplementation : class, TInterface
{
_services.Add(new ServiceDescriptor(typeof(TInterface), typeof(TImplementation), lifetime));
return this;
}
public ServiceContainerBuilder AddSingleton<TInterface>(TInterface instance) where TInterface : class
{
_services.Add(ServiceDescriptor.Singleton(typeof(TInterface), instance));
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(_name))
result.AddError(new MemberName(nameof(_name), typeof(ServiceContainerBuilder)),
new InvalidOperationException("Container name is required"));
if (!_services.Any())
result.AddError(new MemberName(nameof(_services), typeof(ServiceContainerBuilder)),
new InvalidOperationException("At least one service must be registered"));
// Check for duplicate service registrations
var duplicates = _services
.GroupBy(s => s.ServiceType)
.Where(g => g.Count() > 1);
foreach (var dup in duplicates)
result.AddError(new MemberName(nameof(_services), typeof(ServiceContainerBuilder)),
new InvalidOperationException($"Service {dup.Key.Name} is registered multiple times"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<ServiceContainer>>> InstantiateAsync(
Reference<ServiceContainer> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
// Simulate async container initialization
await Task.Delay(50, ct).ConfigureAwait(false);
var container = new ServiceContainer(_name, _services);
reference.Resolve(container);
return Result<Reference<ServiceContainer>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException($"ServiceContainer build failed: {vr.ErrorMessage}");
}
}
// Usage
var containerResult = await new ServiceContainerBuilder()
.Named("WebServices")
.AddService<IUserRepository, UserRepository>(ServiceLifetime.Scoped)
.AddService<IOrderService, OrderService>(ServiceLifetime.Scoped)
.AddSingleton<ILogger>(new ConsoleLogger())
.BuildAsync();
if (containerResult.IsSuccess)
{
var container = containerResult.ValueOrThrow().Resolved();
Console.WriteLine($"Container '{container.Name}' initialized with {container.ServiceCount} services");
}Example 3: Multi-Client API Request Builder
public class ApiClientBuilder : AbstractBuilder<ApiClient>
{
private Uri? _baseUrl;
private TimeSpan _timeout = TimeSpan.FromSeconds(30);
private readonly Dictionary<string, string> _defaultHeaders = new();
private readonly List<ApiEndpoint> _endpoints = new();
public ApiClientBuilder BaseUrl(string url)
{
_baseUrl = new Uri(url);
return this;
}
public ApiClientBuilder Timeout(TimeSpan timeout)
{
_timeout = timeout;
return this;
}
public ApiClientBuilder AddHeader(string name, string value)
{
_defaultHeaders[name] = value;
return this;
}
public ApiClientBuilder MapEndpoint(string name, string path, HttpMethod method)
{
_endpoints.Add(new ApiEndpoint(name, path, method));
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (_baseUrl == null)
result.AddError(new MemberName(nameof(_baseUrl), typeof(ApiClientBuilder)),
new InvalidOperationException("Base URL is required"));
if (_timeout <= TimeSpan.Zero)
result.AddError(new MemberName(nameof(_timeout), typeof(ApiClientBuilder)),
new InvalidOperationException("Timeout must be positive"));
if (!_defaultHeaders.ContainsKey("Content-Type"))
result.AddError(new MemberName(nameof(_defaultHeaders), typeof(ApiClientBuilder)),
new InvalidOperationException("Content-Type header is required"));
if (!_endpoints.Any())
result.AddError(new MemberName(nameof(_endpoints), typeof(ApiClientBuilder)),
new InvalidOperationException("At least one endpoint must be mapped"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<ApiClient>>> InstantiateAsync(
Reference<ApiClient> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
// Simulate async client initialization
await Task.Delay(100, ct).ConfigureAwait(false);
var client = new ApiClient(_baseUrl!)
{
Timeout = _timeout,
DefaultHeaders = _defaultHeaders,
Endpoints = _endpoints
};
reference.Resolve(client);
return Result<Reference<ApiClient>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException($"ApiClient build failed: {vr.ErrorMessage}");
}
}
// Usage
var clientResult = await new ApiClientBuilder()
.BaseUrl("https://api.example.com/v1")
.Timeout(TimeSpan.FromSeconds(60))
.AddHeader("Content-Type", "application/json")
.AddHeader("User-Agent", "MyApp/1.0")
.MapEndpoint("GetUsers", "/users", HttpMethod.Get)
.MapEndpoint("CreateUser", "/users", HttpMethod.Post)
.MapEndpoint("GetUser", "/users/{id}", HttpMethod.Get)
.BuildAsync();
if (clientResult.IsSuccess)
{
var client = clientResult.ValueOrThrow().Resolved();
Console.WriteLine($"API client configured for {client.BaseAddress}");
Console.WriteLine($"Endpoints: {string.Join(", ", client.Endpoints.Select(e => e.Name))}");
}public class ApiClientBuilder : AbstractBuilder<ApiClient>
{
private Uri? _baseUrl;
private TimeSpan _timeout = TimeSpan.FromSeconds(30);
private readonly Dictionary<string, string> _defaultHeaders = new();
private readonly List<ApiEndpoint> _endpoints = new();
public ApiClientBuilder BaseUrl(string url)
{
_baseUrl = new Uri(url);
return this;
}
public ApiClientBuilder Timeout(TimeSpan timeout)
{
_timeout = timeout;
return this;
}
public ApiClientBuilder AddHeader(string name, string value)
{
_defaultHeaders[name] = value;
return this;
}
public ApiClientBuilder MapEndpoint(string name, string path, HttpMethod method)
{
_endpoints.Add(new ApiEndpoint(name, path, method));
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
if (_baseUrl == null)
result.AddError(new MemberName(nameof(_baseUrl), typeof(ApiClientBuilder)),
new InvalidOperationException("Base URL is required"));
if (_timeout <= TimeSpan.Zero)
result.AddError(new MemberName(nameof(_timeout), typeof(ApiClientBuilder)),
new InvalidOperationException("Timeout must be positive"));
if (!_defaultHeaders.ContainsKey("Content-Type"))
result.AddError(new MemberName(nameof(_defaultHeaders), typeof(ApiClientBuilder)),
new InvalidOperationException("Content-Type header is required"));
if (!_endpoints.Any())
result.AddError(new MemberName(nameof(_endpoints), typeof(ApiClientBuilder)),
new InvalidOperationException("At least one endpoint must be mapped"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
protected override async Task<Result<Reference<ApiClient>>> InstantiateAsync(
Reference<ApiClient> reference,
VisitedObjects visitedObjects,
CancellationToken ct)
{
// Simulate async client initialization
await Task.Delay(100, ct).ConfigureAwait(false);
var client = new ApiClient(_baseUrl!)
{
Timeout = _timeout,
DefaultHeaders = _defaultHeaders,
Endpoints = _endpoints
};
reference.Resolve(client);
return Result<Reference<ApiClient>>.Success(reference);
}
protected override Exception BuildException(Result<ValidationResult> validationResult)
{
var vr = validationResult.ValueOrThrow().ToDataAnnotationsValidationResult();
return new InvalidOperationException($"ApiClient build failed: {vr.ErrorMessage}");
}
}
// Usage
var clientResult = await new ApiClientBuilder()
.BaseUrl("https://api.example.com/v1")
.Timeout(TimeSpan.FromSeconds(60))
.AddHeader("Content-Type", "application/json")
.AddHeader("User-Agent", "MyApp/1.0")
.MapEndpoint("GetUsers", "/users", HttpMethod.Get)
.MapEndpoint("CreateUser", "/users", HttpMethod.Post)
.MapEndpoint("GetUser", "/users/{id}", HttpMethod.Get)
.BuildAsync();
if (clientResult.IsSuccess)
{
var client = clientResult.ValueOrThrow().Resolved();
Console.WriteLine($"API client configured for {client.BaseAddress}");
Console.WriteLine($"Endpoints: {string.Join(", ", client.Endpoints.Select(e => e.Name))}");
}Traditional vs. FrenchExDev.Net.Builder vs. Fluent Builders
| Aspect | Traditional | Fluent Builder | FrenchExDev.Net.Builder |
|---|---|---|---|
| Synchronous | Yes | Yes | No (async-first) |
| Validation | Early,fail-fast | Fail-fast | Late, accumulate all errors |
| Circular References | Manual handling | Not supported | Auto-handled via Reference |
| Concurrency | N/A | Manual locking | Built-in single-flight |
| Result Typing | Value only | Value only | Result<Reference |
| Caching | Manual | Manual | Automatic |
| Exception Types | Limited | Limited | Custom (TException) |
| Graph Building | Complex | Not supported | First-class support |
| Learning Curve | Low | Low-Medium | Medium |
| Enterprise Ready | No | Moderate | Yes |
Testing with FrenchExDev.Net.Builder
Because validation and instantiation are separated, builders are highly testable:
[TestFixture]
public class UserBuilderTests
{
[Test]
public async Task BuildAsync_WithValidData_ReturnsSuccess()
{
// Arrange
var builder = new UserBuilder()
.WithEmail("john@example.com")
.WithName("John Doe");
// Act
var result = await builder.BuildAsync();
// Assert
Assert.True(result.IsSuccess);
var user = result.ValueOrThrow().Resolved();
Assert.Equal("john@example.com", user.Email);
Assert.Equal("John Doe", user.Name);
}
[Test]
public async Task BuildAsync_WithMissingEmail_ReturnsFMemberValidationError()
{
// Arrange
var builder = new UserBuilder()
.WithName("John Doe");
// Act
var result = await builder.BuildAsync();
// Assert
Assert.False(result.IsSuccess);
var errors = result.ValidationResult!.ErrorMessage;
Assert.Contains("Email is required", errors);
}
[Test]
public async Task BuildAsync_MultipleCalls_ReturnsCachedReference()
{
// Arrange
var builder = new UserBuilder()
.WithEmail("test@example.com")
.WithName("Test User");
// Act
var result1 = await builder.BuildAsync();
var result2 = await builder.BuildAsync();
// Assert
Assert.True(result1.IsSuccess);
Assert.True(result2.IsSuccess);
// Same reference instance (cached)
var ref1 = result1.ValueOrThrow();
var ref2 = result2.ValueOrThrow();
Assert.Same(ref1, ref2);
}
[Test]
public async Task BuildAsync_WithCancellation_ThrowsOperationCanceledException()
{
// Arrange
var builder = new LongRunningBuilder();
var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(10));
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
() => builder.BuildAsync(cancellationToken: cts.Token));
}
}[TestFixture]
public class UserBuilderTests
{
[Test]
public async Task BuildAsync_WithValidData_ReturnsSuccess()
{
// Arrange
var builder = new UserBuilder()
.WithEmail("john@example.com")
.WithName("John Doe");
// Act
var result = await builder.BuildAsync();
// Assert
Assert.True(result.IsSuccess);
var user = result.ValueOrThrow().Resolved();
Assert.Equal("john@example.com", user.Email);
Assert.Equal("John Doe", user.Name);
}
[Test]
public async Task BuildAsync_WithMissingEmail_ReturnsFMemberValidationError()
{
// Arrange
var builder = new UserBuilder()
.WithName("John Doe");
// Act
var result = await builder.BuildAsync();
// Assert
Assert.False(result.IsSuccess);
var errors = result.ValidationResult!.ErrorMessage;
Assert.Contains("Email is required", errors);
}
[Test]
public async Task BuildAsync_MultipleCalls_ReturnsCachedReference()
{
// Arrange
var builder = new UserBuilder()
.WithEmail("test@example.com")
.WithName("Test User");
// Act
var result1 = await builder.BuildAsync();
var result2 = await builder.BuildAsync();
// Assert
Assert.True(result1.IsSuccess);
Assert.True(result2.IsSuccess);
// Same reference instance (cached)
var ref1 = result1.ValueOrThrow();
var ref2 = result2.ValueOrThrow();
Assert.Same(ref1, ref2);
}
[Test]
public async Task BuildAsync_WithCancellation_ThrowsOperationCanceledException()
{
// Arrange
var builder = new LongRunningBuilder();
var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(10));
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(
() => builder.BuildAsync(cancellationToken: cts.Token));
}
}Inheritance and Shared Validation
For complex hierarchies, use abstract base builders:
public abstract class BaseEntityBuilder<T> : AbstractBuilder<T> where T : BaseEntity, new()
{
protected Guid _id = Guid.NewGuid();
protected DateTime _createdAt = DateTime.UtcNow;
public void SetId(Guid id) => _id = id;
public void SetCreatedAt(DateTime date) => _createdAt = date;
// Common validations for all entities
protected void AddBaseValidations(ValidationResult result)
{
if (_id == Guid.Empty)
result.AddError(new MemberName(nameof(_id), GetType()),
new InvalidOperationException("ID cannot be empty"));
if (_createdAt == default)
result.AddError(new MemberName(nameof(_createdAt), GetType()),
new InvalidOperationException("CreatedAt must be set"));
}
}
public class SpecificEntityBuilder : BaseEntityBuilder<SpecificEntity>
{
private string _name = string.Empty;
public SpecificEntityBuilder WithName(string name)
{
_name = name;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// Add inherited validations
AddBaseValidations(result);
// Add specific validations
if (string.IsNullOrEmpty(_name))
result.AddError(new MemberName(nameof(_name), typeof(SpecificEntityBuilder)),
new InvalidOperationException("Name is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// ... Instantiate and BuildException methods
}public abstract class BaseEntityBuilder<T> : AbstractBuilder<T> where T : BaseEntity, new()
{
protected Guid _id = Guid.NewGuid();
protected DateTime _createdAt = DateTime.UtcNow;
public void SetId(Guid id) => _id = id;
public void SetCreatedAt(DateTime date) => _createdAt = date;
// Common validations for all entities
protected void AddBaseValidations(ValidationResult result)
{
if (_id == Guid.Empty)
result.AddError(new MemberName(nameof(_id), GetType()),
new InvalidOperationException("ID cannot be empty"));
if (_createdAt == default)
result.AddError(new MemberName(nameof(_createdAt), GetType()),
new InvalidOperationException("CreatedAt must be set"));
}
}
public class SpecificEntityBuilder : BaseEntityBuilder<SpecificEntity>
{
private string _name = string.Empty;
public SpecificEntityBuilder WithName(string name)
{
_name = name;
return this;
}
protected override Task<Result<ValidationResult>> ValidateAsync(CancellationToken ct)
{
var result = new ValidationResult();
// Add inherited validations
AddBaseValidations(result);
// Add specific validations
if (string.IsNullOrEmpty(_name))
result.AddError(new MemberName(nameof(_name), typeof(SpecificEntityBuilder)),
new InvalidOperationException("Name is required"));
return Task.FromResult(Result<ValidationResult>.Success(result));
}
// ... Instantiate and BuildException methods
}Conclusion
The Builder pattern remains a cornerstone of maintainable object construction, and FrenchExDev.Net.Builder elevates it to production-grade complexity. By providing:
- Async-first architecture - Natural fit for modern async/await .NET applications
- Comprehensive validation - Accumulate and report all errors at once, not fail-fast
- Automatic reference handling - Circular references and object graphs handled transparently
- Built-in concurrency - Single-flight pattern with caching prevents redundant work
- Type-safe results -
Result<Reference<T>>conveys both success/failure and lazy evaluation - Strong testability - Separated concerns enable unit testing each phase independently
The library excels in enterprise scenarios:
- Configuration objects with many optional parameters and async initialization
- Complex object graphs with parent-child relationships and circular references
- Multi-step initialization requiring async I/O (database, API, file system)
- Concurrent building where multiple requests must coordinate
- Validation-heavy domain models where error accumulation is critical
- Domain-driven design where builders act as factories with rich behavior
When to Use FrenchExDev.Net.Builder
- Building objects with 5+ configuration options
- Async initialization is required
- Circular references exist in your object graph
- Comprehensive validation error reporting needed
- Multiple concurrent builders accessing same resource
- Building test data fixtures that mirror production scenarios
When Traditional Builders Suffice
- Simple synchronous object construction
- Fail-fast validation is appropriate
- No circular references
- Every parameter required (no optional config)
By adopting FrenchExDev.Net.Builder systematically, teams build more resilient, maintainable, and testable .NET applications that handle real-world complexity with elegance.
FrenchExDev.Net.Builder is used throughout the ecosystem: BinaryWrapper generates fluent command builders from CLI help text, the Docker Compose Bundle generates model builders from JSON Schema, and DDD entity factories use AbstractBuilder<T> for validated aggregate construction.