Skip to content

.NET Patterns: Builder, Exceptions & CLI Setup

The classic constructor approach becomes unreadable with many parameters. A User object with 5+ properties forces you to count arguments, guess order, and maintain brittle tests.

The Fluent Builder reads like English:

var user = new UserBuilder()
.WithName("John Doe")
.WithEmail("john.doe@example.com")
.WithAge(30)
.Build();

Each method returns this, enabling method chaining. Your test intent becomes crystal clear—no squinting at constructor arguments.

AspectClassic ConstructorFluent Builder
Readabilitynew User("John Doe", "john@...", 30, "Admin")Chained readable calls
Parameter OrderEasy to mess upSelf-documenting
Test MaintenanceBreaks when signature changesFlexible, optional params
Complex ObjectsConstructor telescoping chaosGraceful, modular
public class UserBuilder
{
private string _name = "Guest";
private string _email = "guest@example.com";
private int _age = 18;
public UserBuilder WithName(string name)
{
_name = name;
return this;
}
public UserBuilder WithEmail(string email)
{
_email = email;
return this;
}
public UserBuilder WithAge(int age)
{
_age = age;
return this;
}
public User Build() => new User(_name, _email, _age);
}

Native Exception is too generic. Your API handler can’t distinguish a 404 from a 500, and logs become noise. Custom exceptions carry HTTP status codes and error codes.

1. Abstract Base Class — Clear inheritance, centralized logic:

public abstract class CustomExceptionBase : Exception
{
public string CustomMessage { get; }
public HttpStatusCode StatusCode { get; }
public string ErrorCode { get; }
protected CustomExceptionBase(string message, string errorCode,
HttpStatusCode statusCode)
{
CustomMessage = message;
ErrorCode = errorCode;
StatusCode = statusCode;
}
}

2. Const Strings for Error Codes — Centralized, reusable:

public static class ErrorCodes
{
public const string NotFound = "NOT_FOUND";
public const string Unauthorized = "UNAUTHORIZED";
public const string Conflict = "CONFLICT";
}

3. Specific Exception Types — Type-safe handling:

public class NotFoundException : CustomExceptionBase
{
public NotFoundException(string message)
: base(message, ErrorCodes.NotFound, HttpStatusCode.NotFound) { }
}
public class UnauthorizedException : CustomExceptionBase
{
public UnauthorizedException(string message)
: base(message, ErrorCodes.Unauthorized, HttpStatusCode.Unauthorized) { }
}
PatternProsCons
Abstract BaseDRY, enforces structureRequires all exceptions inherit
Generics (T)Type-safe, flexibleMore complex syntax
Const StringsReusable error codesNeed manual mapping

Professional projects separate source and tests for clarity. dotnet CLI automates this setup.

Terminal window
# Create Aspire starter template with Redis
dotnet new aspire-starter --use-redis-cache --output AspireSample
cd AspireSample
# Create directory structure
mkdir src tests
# Move projects into organized folders
mv ExpenseTracker.ApiService src/
mv ExpenseTracker.Tests tests/
# Recreate solution (new structure)
rm AspireSample.sln
dotnet new sln -n AspireSample
# Register projects to solution
dotnet sln AspireSample.sln add src/ExpenseTracker.ApiService/ExpenseTracker.ApiService.csproj
dotnet sln AspireSample.sln add tests/ExpenseTracker.Tests/ExpenseTracker.Tests.csproj
# Build and verify
dotnet restore && dotnet build
AspireSample/
├── src/
│ ├── ExpenseTracker.ApiService/
│ │ └── ExpenseTracker.ApiService.csproj
│ └── ExpenseTracker.Shared/
├── tests/
│ └── ExpenseTracker.Tests/
│ └── ExpenseTracker.Tests.csproj
└── AspireSample.sln

Next steps: Implement Fluent Builders in your test utilities, define custom exceptions in a shared layer, then scaffold your project with dotnet new to enforce structure from day one.