🚀 C# Tip — Constructors & Object Initialization (Classic → Modern) in .NET
C# constructors have evolved a lot in the last few versions. Today, you can choose between classic constructors, records, primary constructors, and required members—each with different trade-offs.
Here’s a practical guide to what matters in real .NET code 👇
1️⃣ Classic constructors (still the baseline)
Use when you need:
- multiple overloads
- validation / invariants
- complex initialization logic
public sealed class Order { public Guid Id { get; } public decimal Total { get; } public Order(Guid id, decimal total) { if (total < 0) throw new ArgumentOutOfRangeException(nameof(total)); Id = id; Total = total; } }
2️⃣ Records: “constructor-first” modeling
Records are great for immutable data models (DTOs, messages, snapshots):
public record OrderDto(Guid Id, decimal Total);
They generate value-based equality and keep your model concise.
3️⃣ Primary Constructors (C# 12+): less ceremony (great for DI)
Primary constructors let you declare constructor parameters in the type declaration, and use them throughout the type.
public sealed class OrdersService(IOrderRepository repo, ILogger<OrdersService> log) { public Task<Order?> GetAsync(Guid id) => repo.GetByIdAsync(id); }
✅ Ideal for “DI services with only assignments”
⚠️ If initialization/validation grows, a classic constructor can be clearer
4️⃣ required members (C# 11+): enforce initialization at compile time
With required, the compiler forces callers to initialize important state—either via object initializer or constructor rules.
public class Customer { public required string Name { get; init; } public string? Email { get; init; } } var c = new Customer { Name = "Diego" }; // OK
✅ Great for DTOs / config objects
⚠️ Be careful: it’s a compile-time contract—design your public constructors with intent
5️⃣ Object initializers keep getting better (C# 13 highlight)
C# 13 improved object initializers with implicit “from the end” index (^) for single-dimensional collections.
var countdown = new TimerRemaining { buffer = { [^1] = 0, [^2] = 1, [^3] = 2 } };
This is small, but it improves readability in initialization-heavy code.
🧠 How to choose (quick rule of thumb)
- Classic constructor → invariants + multiple ways to create the object
- Record → immutable “data carrier”
- Primary constructor → lean services / simple state wiring (DI-friendly)
- required + init → enforce “must set” properties without constructor overload noise
- Object initializers → great for DTO/config/build-up patterns
🎯 Final takeaway
Modern C# gives you multiple ways to express “how an object is born.”
Pick the one that makes intent obvious and invalid states hard.
#dotnet #csharp #architecture #cleanCode #softwareengineering #aspnetcore