Overview

For long-term success in today's fast-paced development landscape, it is essential to write clean and maintainable code. The article examines practical clean coding principles tailored to C# 13, showing how modern language features can make coding easier to read, test, and maintain. In this book, readers will learn how to write efficient, professional-grade C# code by utilizing primary constructors, immutable records, and Clean Architecture. This guide will teach you the habits that lead to better software quality and team productivity, regardless of whether you're building enterprise applications or personal projects.

Why Clean Code Matters

It's a fundamental practice for building scalable, maintainable, and reliable software. Whether you're working on an internal tool or an enterprise-grade application, clean code:

In C# 13, developers have access to modern features that make writing clean, expressive, and efficient code easier than ever.

What is Clean Code?

Clean code is:

C# 13 Features That Promote Clean Code

Primary Constructors for Classes

As part of C# 13, primary constructors for classes are introduced, allowing them to be more concise and expressive:

 // <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:Customer.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Models;

public class Customer
{
    public int Id { get; init; }
    public string Name { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;

    public Customer(int id, string name, string email)
    {
        Id = id;
        Name = name;
        Email = email;
    }
}
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:Invoice.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Models;

 
public class Invoice(int invoiceId, string customer, decimal amount)
{
    public int InvoiceId { get; } = invoiceId;
    public string Customer { get; } = customer;
    public decimal Amount { get; } = amount;
}
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:Product.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Models;

public record Product(string Name, decimal Price);
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:User.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Models;

public class User
{
    public int Id { get; init; }
    public string Name { get; init; }=string.Empty;

    public string Email { get; init; }=string.Empty;
}

Improved String Interpolation

The following enhancements support improved formatting and efficiency:

 // <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:Program.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
#nullable enable
var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();


// Minimal API endpoint replacing InvoiceController
app.MapGet("/api/invoice/{id}", (int id) => {
    var amount = 1234.56m;
    var dueDate = DateTime.Today.AddDays(30);
    var message = $"Total: {amount:C2}, Due: {dueDate:yyyy-MM-dd}";

    return Results.Ok(new { id, message });
});

app.Run();

Params Span<T> Support

Handling large datasets or memory-sensitive operations efficiently:

 // <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:Printer.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Utilities;

public static class Printer
{
    public static void Print(params ReadOnlySpan<string> messages)
    {
        foreach (var message in messages)
        {
            Console.WriteLine(message);
        }
    }
}
 

Naming Conventions and Method Design

// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:CustomerService.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

using CleanCodeWithCSharp13.Domain.Interfaces;
using CleanCodeWithCSharp13.Domain.Models;


#nullable enable
namespace CleanCodeWithCSharp13.Application.Services;

public class CustomerService
{
    private readonly ICustomerRepository _repository;

    public CustomerService(ICustomerRepository repository)
    {
        _repository = repository;
    }

    public void AddCustomer(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        if (string.IsNullOrWhiteSpace(customer.Name))
            throw new ArgumentException("Customer name must not be empty.", nameof(customer.Name));

        _repository.Add(customer);
    }

    public bool TryGetCustomer(int id, out Customer? customer)
    {
        customer = _repository.GetById(id);
        return customer is not null;
    }
}

v
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:OrderService.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

using CleanCodeWithCSharp13.Domain.Interfaces;
using CleanCodeWithCSharp13.Domain.Models;

namespace CleanCodeWithCSharp13.Application.Services;

public class OrderService {
    private readonly IOrderRepository _orderRepository;
    private readonly TaxService _taxService;
    private readonly NotificationService _notificationService;

    public OrderService(
        IOrderRepository orderRepository,
        TaxService taxService,
        NotificationService notificationService) {
        _orderRepository = orderRepository;
        _taxService = taxService;
        _notificationService = notificationService;
    }

    public void PlaceOrder(Order order) {
        ArgumentNullException.ThrowIfNull(order);

        ValidateOrder(order);

        var taxAmount = _taxService.CalculateTaxAmount(order.Invoice);
        order.TotalAmount = order.Invoice.Amount + taxAmount;

        _orderRepository.Add(order);

        // Notify user asynchronously without blocking
        _ = _notificationService.NotifyUser(order.CustomerEmail);
    }

    private static void ValidateOrder(Order order) {
        if (string.IsNullOrWhiteSpace(order.CustomerEmail))
            throw new ArgumentException("Customer email must not be empty.", nameof(order.CustomerEmail));

        if (order.Invoice == null)
            throw new ArgumentNullException(nameof(order.Invoice));

        if (order.Invoice.Amount <= 0)
            throw new ArgumentException("Invoice amount must be greater than zero.", nameof(order.Invoice));
    }
}
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:TaxService.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

using CleanCodeWithCSharp13.Domain.Models;

namespace CleanCodeWithCSharp13.Application.Services;

public class TaxService
{
    public decimal CalculateTaxAmount(Invoice invoice)
    {
        if (invoice == null)
            throw new ArgumentNullException(nameof(invoice));

        const decimal taxRate = 0.2m; // 20% tax
        return invoice.Amount * taxRate;
    }
}

using CleanCodeWithCSharp13.Domain.Models;

namespace CleanCodeWithCSharp13.Application.Services;

public class TaxService
{
    public decimal CalculateTaxAmount(Invoice invoice)
    {
        if (invoice == null)
            throw new ArgumentNullException(nameof(invoice));

        const decimal taxRate = 0.2m; // 20% tax
        return invoice.Amount * taxRate;
    }
}
var result = CalculateTaxAmount(invoice);

Avoid vague or abbreviated names:

var x = DoWork(obj);

Embrace Immutability and Records

The following reasons make immutable objects easier to test and reason about:

// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:Product.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Models;

public record Product(string Name, decimal Price);For mutable classes, use init setters and readonly fields:
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:User.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Models;

public class User
{
    public int Id { get; init; }
    public string Name { get; init; }=string.Empty;

    public string Email { get; init; }=string.Empty;
}
var product = new Product("Laptop", 999.99m);
// product.Price = 1099.99m; ❌ not allowed, record is immutable
var user = new User { Id = 1, Name = "Alice" };
// user.Name = "Bob"; ❌ not allowed, init-only

Exception Handling and Null Safety

 throw new InvalidOperationException("Order ID is invalid.");

Use null-safe coding practices:

 ArgumentNullException.ThrowIfNull(customer);

Whenever possible, use TryGetValue, TryParse, and pattern matching to ensure safety and clarity.

Side-Effect-Free Code and Testability

Example:

// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:ICustomerRepository.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

#nullable enable
using CleanCodeWithCSharp13.Domain.Models;

namespace CleanCodeWithCSharp13.Domain.Interfaces;

public interface ICustomerRepository
{
    void Add(Customer customer);
    Customer? GetById(int id);
}

// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:IOrderRepository.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

using CleanCodeWithCSharp13.Domain.Models;

namespace CleanCodeWithCSharp13.Domain.Interfaces;

public interface IOrderRepository
{
    void Add(Order order);
    Order? GetById(int id);
}
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:IEmailSender.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
namespace CleanCodeWithCSharp13.Domain.Interfaces;

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body);
}
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:CustomerService.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

using CleanCodeWithCSharp13.Domain.Interfaces;
using CleanCodeWithCSharp13.Domain.Models;


#nullable enable
namespace CleanCodeWithCSharp13.Application.Services;

public class CustomerService
{
    private readonly ICustomerRepository _repository;

    public CustomerService(ICustomerRepository repository)
    {
        _repository = repository;
    }

    public void AddCustomer(Customer customer)
    {
        ArgumentNullException.ThrowIfNull(customer);

        if (string.IsNullOrWhiteSpace(customer.Name))
            throw new ArgumentException("Customer name must not be empty.", nameof(customer.Name));

        _repository.Add(customer);
    }

    public bool TryGetCustomer(int id, out Customer? customer)
    {
        customer = _repository.GetById(id);
        return customer is not null;
    }
}
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:OrderService.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

using CleanCodeWithCSharp13.Domain.Interfaces;
using CleanCodeWithCSharp13.Domain.Models;

namespace CleanCodeWithCSharp13.Application.Services;

public class OrderService {
    private readonly IOrderRepository _orderRepository;
    private readonly TaxService _taxService;
    private readonly NotificationService _notificationService;

    public OrderService(
        IOrderRepository orderRepository,
        TaxService taxService,
        NotificationService notificationService) {
        _orderRepository = orderRepository;
        _taxService = taxService;
        _notificationService = notificationService;
    }

    public void PlaceOrder(Order order) {
        ArgumentNullException.ThrowIfNull(order);

        ValidateOrder(order);

        var taxAmount = _taxService.CalculateTaxAmount(order.Invoice);
        order.TotalAmount = order.Invoice.Amount + taxAmount;

        _orderRepository.Add(order);

        // Notify user asynchronously without blocking
        _ = _notificationService.NotifyUser(order.CustomerEmail);
    }

    private static void ValidateOrder(Order order) {
        if (string.IsNullOrWhiteSpace(order.CustomerEmail))
            throw new ArgumentException("Customer email must not be empty.", nameof(order.CustomerEmail));

        if (order.Invoice == null)
            throw new ArgumentNullException(nameof(order.Invoice));

        if (order.Invoice.Amount <= 0)
            throw new ArgumentException("Invoice amount must be greater than zero.", nameof(order.Invoice));
    }
}

// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:TaxService.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>

using CleanCodeWithCSharp13.Domain.Models;

namespace CleanCodeWithCSharp13.Application.Services;

public class TaxService
{
    public decimal CalculateTaxAmount(Invoice invoice)
    {
        if (invoice == null)
            throw new ArgumentNullException(nameof(invoice));

        const decimal taxRate = 0.2m; // 20% tax
        return invoice.Amount * taxRate;
    }
}
// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:NotificationService.cs
// Author: Ziggy Rafiq
// Date: 01st June 2025
// </auto-generated>
using CleanCodeWithCSharp13.Domain.Interfaces;

namespace CleanCodeWithCSharp13.Application.Services;
public class NotificationService
{
    private readonly IEmailSender _emailSender;

    public NotificationService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public async Task NotifyUser(string userEmail)
    {
        await _emailSender.SendAsync(userEmail, "Welcome", "Thanks for signing up!");
    }
}

Clean Architecture in Practice

The codebase should be well-structured so that it won't become complex as it grows. Clean Architecture promotes the separation of concerns and long-term maintainability. As a result, each module has a single, well-defined responsibility, which makes it easier for developers to understand, test, and modify. Clean Architecture helps developers build systems that are resilient to change and scalable.

Structure Example

Automate Code Quality

Ensure clean code standards are enforced by using the following tools:

Final Thoughts

Clean code is a habit. It is the result of deliberate practices, code reviews, refactoring, and a focus on readability and correctness.

Recap

The code examples used in this article are stored in my Ziggy Rafiq GitHub Repository.