ASP.NET Hosting

Clean Code and Best Practices with C#

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:

  • Reduces onboarding time for new developers and increases readability
  • Makes testing easier and minimises bugs
  • Reduces technical debt and simplifies future changes
  • Collaboration among team members is encouraged

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:

  • Readable – easily understood by others
  • Intentional – names, methods, and structures reflect clear purpose
  • Modular – small, focused functions and classes
  • Testable – easy to verify correctness
  • Consistent – follows established patterns and formatting

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
// </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
// </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
// </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
// </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
// </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
// </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

  • Names that are descriptive and reveal the intention are preferred:
// <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
// </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
// </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);
  • Keep methods short: A single responsibility should be the focus of each method, and it should be as short as possible, ideally less than 20 lines.

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
// </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

  • The catch must be avoided (Exception) unless you are rethrowing or logging the throw
  • Exceptions should be thrown with clear messages:
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

  • Global variables and static mutable variables should be avoided
  • Better decoupling and testing can be achieved by using dependency injection
  • Pure functions (same input, same output) should be designed whenever possible.

Example:

// <auto-generated>
// This file is part of the CleanCodeWithCSharp13 project.
// File:ICustomerRepository.cs
// </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
// </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
// </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
// </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
// </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
// </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
// </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:

  • StyleCop.Analyzers: Catch naming/style violations with StyleCop.Analyzers
  • .editorconfig: Code formatting rules are defined and enforced in .editorconfig
  • dotnet format: Formatting issues in the dotnet format can be fixed automatically
  • SonarQube / Roslyn Analysers: Code smells and potential bugs can be detected using SonarQube / Roslyn Analysers
  • CI Pipelines: Builds should be stopped after lint/test failures in CI pipelines to ensure consistent code quality

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

  • C# 13 features, such as primary constructors, interpolated strings, and records, can be used
  • Methods that reveal intentions should be small and simple
  • Immutability should be prioritised over state minimisation
  • Clean Architecture concerns should be separated
  • Checking and testing styles automatically

ASP.NET Core 10.0 Hosting Recommendation

HostForLIFE.eu
HostForLIFE.eu is a popular recommendation that offers various hosting choices. Starting from shared hosting to dedicated servers, you will find options fit for beginners and popular websites. It offers various hosting choices if you want to scale up. Also, you get flexible billing plans where you can choose to purchase a subscription even for one or six months.