DEV Community

Seigo Kitamura
Seigo Kitamura

Posted on

Part 1 - Core Foundations for Enterprise C#: OOP & SOLID, Clean Architecture, and Type Semantics

Excerpt

This post distills the core C# foundations enterprise teams expect: applying OOP & SOLID in real systems, enforcing clean architecture boundaries, and choosing the right type semantics (class vs record vs struct). We also cover string immutability, when to use StringBuilder, and LINQ pitfalls that affect performance and correctness.


Core Foundations for Enterprise C


Object-Oriented Programming (OOP): Concepts & Pillars

What is OOP and how does it help us?

Object-Oriented Programming (OOP) is a paradigm that organizes software design around data, or objects, rather than functions and logic. OOP helps us build scalable, maintainable, and reusable code by modeling real-world entities and their interactions.

The Five Pillars of OOP:

  1. Encapsulation: Bundling data and methods that operate on that data within one unit (class), and restricting direct access to some of the object's components.
  2. Abstraction: Hiding complex implementation details and exposing only the necessary features.
  3. Inheritance: Creating new classes from existing ones, inheriting fields and behaviors.
  4. Polymorphism: Allowing objects to be treated as instances of their parent class, enabling one interface to be used for different underlying forms (data types).
  5. Composition (modern best practice): Building complex types by combining objects, favoring "has-a" relationships over "is-a" inheritance.

1. Encapsulation

public class BankAccount
{
    private decimal _balance; // private field

    public void Deposit(decimal amount)
    {
        if (amount > 0) _balance += amount;
    }

    public decimal Balance => _balance; // read-only property
}
Enter fullscreen mode Exit fullscreen mode

2. Abstraction

public abstract class Shape
{
    public abstract double Area();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    public override double Area() => Math.PI * Radius * Radius;
}
Enter fullscreen mode Exit fullscreen mode

3. Inheritance

public class Animal
{
    public void Eat() => Console.WriteLine("Eating...");
}

public class Dog : Animal
{
    public void Bark() => Console.WriteLine("Woof!");
}

var dog = new Dog();
dog.Eat();
dog.Bark();
Enter fullscreen mode Exit fullscreen mode

4. Polymorphism

public class Cat : Animal
{
    public void Meow() => Console.WriteLine("Meow!");
}

public void MakeAnimalSpeak(Animal animal)
{
    animal.Eat();
}

MakeAnimalSpeak(new Dog());
MakeAnimalSpeak(new Cat());
Enter fullscreen mode Exit fullscreen mode

5. Composition

public class Engine
{
    public void Start() => Console.WriteLine("Engine started.");
}

public class Car
{
    private readonly Engine _engine = new Engine();
    public void Start() => _engine.Start();
}
Enter fullscreen mode Exit fullscreen mode


OOP & SOLID (what scales in real systems)

Key ideas

  • Favor composition over inheritance; program to interfaces for testability.
  • SOLID principles guide large codebases:
    • Single Responsibility → keep classes focused.
    • Open/Closed → extend via interfaces/strategies.
    • Liskov Substitution → subtypes should be substitutable.
    • Interface Segregation → small, purpose-fit interfaces.
    • Dependency Inversion → depend on abstractions; use DI containers.

Example: Dependency Inversion + Interface Segregation

public interface IEmailSender
{
    Task SendAsync(string to, string subject, string body, CancellationToken ct);
}

public class SmtpEmailSender : IEmailSender
{
    public Task SendAsync(string to, string subject, string body, CancellationToken ct)
    {
        return Task.CompletedTask;
    }
}

public class NotificationService
{
    private readonly IEmailSender _sender;
    public NotificationService(IEmailSender sender) => _sender = sender;

    public Task NotifyPasswordResetAsync(string userEmail, CancellationToken ct) =>
        _sender.SendAsync(userEmail, "Reset Your Password", "Link...", ct);
}
Enter fullscreen mode Exit fullscreen mode

Immutability reduces bugs

public record ResetRequest(string Email, DateTime RequestedAt); // immutable DTO

public class User {
    public string Name { get; init; } // init-only setter for object initializers
    public string Email { get; init; }
}
Enter fullscreen mode Exit fullscreen mode

Clean Architecture Boundaries (controllers, application, domain, infra)

Keep layers focused and avoid leaking concerns across boundaries.

+-----------------------------------------------------------+
|                   Presentation (Controllers)              |
|      - map HTTP <-> application use cases                 |
+-----------------------------------------------------------+
|                 Application (Services)                    |
|      - orchestrates use cases, coordinates domain         |
+-----------------------------------------------------------+
|                     Domain (Core)                         |
|      - entities, value objects, policies                  |
+-----------------------------------------------------------+
|               Infrastructure (Adapters)                   |
|      - EF Core, HTTP clients, storage, messaging          |
+-----------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Controller → Service → Domain

[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase {
    private readonly IUserService _service;
    public UsersController(IUserService service) => _service = service;

    [HttpPost("reset")]
    public async Task<IActionResult> Reset([FromBody] ResetRequest req, CancellationToken ct) {
        await _service.RequestPasswordResetAsync(req.Email, ct);
        return Accepted();
    }
}

public interface IUserService {
    Task RequestPasswordResetAsync(string email, CancellationToken ct);
}
Enter fullscreen mode Exit fullscreen mode

Types: class vs record vs struct (including record struct)

When to choose what

  • class → Reference type; polymorphism, inheritance, services/entities. Default equality is reference.
  • record → Reference type with value-based equality + with expressions; ideal for DTOs/value models.
  • struct → Value type; lightweight and allocation-free for tiny models. Avoid large structs (copy cost).
  • record struct → Value type with record features (value equality) for small immutable models.

Equality differences

public class UserClass { public string Name { get; init; } }
public record UserRecord(string Name);

var c1 = new UserClass { Name = "A" };
var c2 = new UserClass { Name = "A" };
Console.WriteLine(Equals(c1, c2)); // False (reference equality)

var r1 = new UserRecord("A");
var r2 = new UserRecord("A");
Console.WriteLine(Equals(r1, r2)); // True (value-based equality)
Enter fullscreen mode Exit fullscreen mode

Records: non-destructive mutation

public record Person(string Name, int Age);
var p1 = new Person("Alice", 30);
var p2 = p1 with { Age = 31 };
Enter fullscreen mode Exit fullscreen mode

Structs: keep tiny and often immutable

public readonly struct Point {
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) { X = x; Y = y; }
}
Enter fullscreen mode Exit fullscreen mode

Strings & Collections (immutability, StringBuilder, LINQ pitfalls)

Strings are immutable → avoid heavy concatenation with +

var sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.Append(i).Append(',');
var result = sb.ToString();
Enter fullscreen mode Exit fullscreen mode

LINQ: deferred execution & materialization

var query = numbers.Where(n => n % 2 == 0); // deferred
numbers.Add(2); // changes the source before enumeration
var list = query.ToList(); // materialized; includes newly added 2
Enter fullscreen mode Exit fullscreen mode

Pitfalls & tips

  • Be mindful of multiple enumerations (cache with ToList() when necessary).
  • LINQ in hot loops can add allocations; measure before optimizing.

CTA

If you found this useful, check out Part 2 (Performance & Concurrency Essentials) and Part 3 (Production-Ready Practices). Post your own tips or war stories in the comments—let’s build a living checklist for backend engineers.


Series Navigation

Top comments (0)