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 useStringBuilder, and LINQ pitfalls that affect performance and correctness.
Core Foundations for Enterprise C
- Object-Oriented Programming (OOP): Concepts & Pillars
- OOP & SOLID (what scales in real systems)
- Clean Architecture Boundaries (controllers, application, domain, infra)
- Types: class vs record vs struct (including record struct)
- Strings & Collections (immutability, StringBuilder, LINQ pitfalls)
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:
- 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.
- Abstraction: Hiding complex implementation details and exposing only the necessary features.
- Inheritance: Creating new classes from existing ones, inheriting fields and behaviors.
- Polymorphism: Allowing objects to be treated as instances of their parent class, enabling one interface to be used for different underlying forms (data types).
- 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
}
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;
}
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();
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());
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();
}
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);
}
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; }
}
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 |
+-----------------------------------------------------------+
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);
}
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 +
withexpressions; 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)
Records: non-destructive mutation
public record Person(string Name, int Age);
var p1 = new Person("Alice", 30);
var p2 = p1 with { Age = 31 };
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; }
}
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();
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
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)