A constructor is a special method that runs when an object is created with new. Its job is to ensure the object is always in a valid, usable state from the moment it exists — no half-constructed objects. Without a constructor, callers must remember to set required properties after creation, which is error-prone. With a constructor that requires the mandatory data, an object can never exist in an invalid state. This invariant enforcement is the core principle of good OOP design.
Parameterless and Parameterised Constructors
public class Order
{
// ── Properties ─────────────────────────────────────────────────────────
public int OrderId { get; }
public string CustomerId { get; }
public DateTime OrderDate { get; }
public List<OrderItem> Items { get; } = new();
// ── Parameterised constructor — required data enforced ─────────────────
public Order(int orderId, string customerId)
{
if (orderId <= 0)
throw new ArgumentException("OrderId must be positive.", nameof(orderId));
if (string.IsNullOrWhiteSpace(customerId))
throw new ArgumentException("CustomerId is required.", nameof(customerId));
OrderId = orderId;
CustomerId = customerId.Trim();
OrderDate = DateTime.UtcNow;
}
// ── Parameterless constructor for EF Core / model binding ──────────────
// EF Core requires a parameterless constructor to materialise objects
// Mark private or protected to discourage external use
private Order() { } // EF Core uses reflection to call this
public void AddItem(OrderItem item)
{
ArgumentNullException.ThrowIfNull(item);
Items.Add(item);
}
}
// Creating instances
var order = new Order(orderId: 42, customerId: "CUST-001");
Console.WriteLine(order.OrderDate); // current UTC time
// var bad = new Order(); // ← cannot call private parameterless constructor
private or protected parameterless constructor solely for EF Core. This keeps the domain logic intact while satisfying the ORM.this() to avoid duplicating initialisation logic across multiple constructors. The more specific constructor does the real work; simpler constructors call it with sensible defaults: public Order(string customerId) : this(GenerateId(), customerId) { }. This ensures all validation and field assignment happens in one place. If you add a new required field later, you change only the most complete constructor.InitialiseAsync() methods or inside the controller action itself. ASP.NET Core’s dependency injection creates service instances through constructors, so expensive work in constructors hurts startup time and makes testing difficult.Constructor Chaining with this()
public class BlogPost
{
public int Id { get; }
public string Title { get; }
public string Body { get; }
public string AuthorId { get; }
public DateTime CreatedAt { get; }
// ── Most complete constructor — all validation here ───────────────────
public BlogPost(int id, string title, string body, string authorId, DateTime createdAt)
{
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("Title is required.");
if (string.IsNullOrWhiteSpace(authorId))
throw new ArgumentException("AuthorId is required.");
Id = id;
Title = title.Trim();
Body = body?.Trim() ?? string.Empty;
AuthorId = authorId;
CreatedAt = createdAt;
}
// ── Convenience constructor — chains to the full one ──────────────────
public BlogPost(int id, string title, string body, string authorId)
: this(id, title, body, authorId, DateTime.UtcNow) // calls above constructor
{ }
// ── Minimal constructor (for new posts) ───────────────────────────────
public BlogPost(string title, string authorId)
: this(0, title, string.Empty, authorId) // chains again
{ }
}
// All three constructors work; validation runs in all cases
var p1 = new BlogPost(1, "Hello", "Body", "auth-1", new DateTime(2025, 1, 1));
var p2 = new BlogPost(2, "Hello", "Body", "auth-1"); // uses UtcNow
var p3 = new BlogPost("Hello", "auth-1"); // minimal — id=0, empty body
Primary Constructors (C# 12)
// Primary constructor — parameters defined on the class declaration itself
// Perfect for simple data containers and services with injected dependencies
public class EmailService(ISmtpClient smtpClient, ILogger<EmailService> logger)
{
// smtpClient and logger are available as fields throughout the class
public async Task SendAsync(string to, string subject, string body)
{
logger.LogInformation("Sending email to {To}", to);
await smtpClient.SendAsync(to, subject, body);
}
}
// ── This replaces the boilerplate constructor pattern: ────────────────────────
// private readonly ISmtpClient _smtpClient;
// private readonly ILogger<EmailService> _logger;
// public EmailService(ISmtpClient smtpClient, ILogger<EmailService> logger)
// {
// _smtpClient = smtpClient;
// _logger = logger;
// }
// The primary constructor eliminates this entirely for simple injection scenarios
Common Mistakes
Mistake 1 — Doing async work in a constructor
❌ Wrong — constructors cannot be awaited:
public UserService(IUserRepository repo)
{
_users = repo.GetAllAsync().Result; // blocks the thread — deadlock risk!
}
✅ Correct — load data in a separate async method, not the constructor.
Mistake 2 — Not providing parameterless constructor for EF Core entities
EF Core cannot materialise objects if there is no accessible parameterless constructor. Either add a private parameterless constructor or use EF Core 7+ constructor injection by matching parameter names to property names.