C# Clean Code
2026/04/032 min read
bookmark this
Introduction
Production code fails. Clean code does not pretend failures do not exist; it handles them explicitly and consistently.
1) Use exceptions correctly
Do not use exceptions for normal control flow
// BAD
public int Parse(string input)
{
try { return int.Parse(input); }
catch { return -1; }
}
// GOOD
public int? Parse(string input)
=> int.TryParse(input, out var result) ? result : null;
Throw specific exceptions
public class OrderNotFoundException(int orderId)
: Exception($"Order with ID {orderId} was not found.")
{
public int OrderId { get; } = orderId;
}
Specific exception types improve logging, diagnosis, and mapping to HTTP responses.
2) Never swallow exceptions silently
try
{
await ProcessOrderAsync(orderId, ct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
If you catch, either:
- recover meaningfully, or
- enrich and rethrow
3) Standardize API errors with ProblemDetails
public class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
: IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext context,
Exception exception,
CancellationToken ct)
{
logger.LogError(exception, "Unhandled exception");
var details = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An unexpected error occurred",
Type = "https://tools.ietf.org/html/rfc9110#section-15.6.1"
};
context.Response.StatusCode = details.Status.Value;
await context.Response.WriteAsJsonAsync(details, ct);
return true;
}
}
A consistent response structure helps API clients and monitoring tools.
4) Validate input at boundaries
Boundary validation keeps invalid data from leaking deep into the domain.
API request validation (FluentValidation)
public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty();
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(i => i.Quantity).GreaterThan(0);
item.RuleFor(i => i.Price).GreaterThan(0);
});
}
}
Guard clauses inside services/domain
public class OrderService(IOrderRepository repository)
{
public async Task<Order> GetByIdAsync(int id, CancellationToken ct)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(id);
return await repository.GetByIdAsync(id, ct)
?? throw new OrderNotFoundException(id);
}
}
5) Prefer immutability for safety
public record OrderDto(int Id, string CustomerName, decimal Total, DateTime CreatedAt);
public readonly record struct OrderId(int Value);
public readonly record struct CustomerId(int Value);
Strong types and immutable models reduce accidental state bugs.
Summary
Reliable C# systems combine:
- clear validation boundaries
- specific exceptions
- structured error responses
- immutable and strongly-typed models
This approach makes failures understandable and recoverable.