Back to Blog
C#

C# Constructor Usage: Patterns and Pitfalls

c# constructor usage: Learn practical C# constructor usage: syntax, overloading, static constructors, primary constructors, and constructor injection with real-world e...

C# programmingobject initializationdependency injectiondesign patterns
Illustration of a C# class showing a constructor method with parameter passing and object initialization.

C# constructor usage determines how objects are initialized, what state they start with, and how dependencies enter a class. A constructor is a method that runs when an instance is created, and its signature defines the required inputs for that initialization. Misusing constructors can lead to null references, hidden dependencies, or hard-to-test code, so getting the basics right matters more than it seems.

The most direct form of a constructor is a parameterless one. If you do not declare any constructor, the compiler provides a default parameterless constructor that initializes fields to their default values. Once you declare any constructor—even one with parameters—the implicit default constructor is no longer generated. This is a common source of compile errors when you add a constructor with parameters and then accidentally try to call new MyClass() elsewhere.

public class ReportBuilder { public ReportBuilder(string reportPath) { _reportPath = reportPath; } private readonly string _reportPath; }

In this example, new ReportBuilder() will not compile because the only available constructor requires a string. The compiler does not assume you want a parameterless version. If you need both, you must explicitly add a parameterless constructor, usually to provide sensible defaults.

Overloading Constructors for Flexibility

Constructor overloading lets you provide multiple initialization paths. The goal is not to create every possible combination, but to give callers a clear way to construct an object with the minimum required data while optionally accepting additional configuration.

public class SqlConnectionString { public SqlConnectionString(string server, string database) : this(server, database, "defaultPooling=true") { } public SqlConnectionString(string server, string database, string options) { Server = server; Database = database; Options = options; } }

Here the two-parameter constructor delegates to the three-parameter constructor using : this(...). This reduces duplication and keeps the validation logic in one place. Overloading is most useful when you have a primary constructor that does the real work and convenience constructors that fill in defaults.

Object Initializers vs. Constructors

Object initializers let you set public properties after the constructor runs. They are useful when you want to construct an object with a parameterless constructor and then assign a few properties without writing multiple statements.

var settings = new AppSettings { CacheTimeoutMinutes = 15, RetryCount = 3 };

Object initializers do not replace constructors. They are a syntactic shortcut for assignment. The constructor still controls the initial state before the initializer runs. Use object initializers when the properties are truly optional and the object is valid without them. If a property must be set for the object to function correctly, that property should be a required constructor parameter.

Static Constructors and Type Initialization

A static constructor runs once per type, not per instance. It is triggered the first time the type is accessed, either by creating an instance or by referencing a static member. Static constructors are useful for initializing static fields that require more than a simple assignment.

public class ConfigurationManager { public static readonly Settings GlobalSettings; static ConfigurationManager() { GlobalSettings = LoadFromDisk(); } private static Settings LoadFromDisk() { // Load and parse configuration file } }

The static constructor runs exactly once, but the exact timing is implementation-defined. It can run when the type is first used, or earlier if the runtime decides to run it eagerly. Do not rely on a precise ordering of static constructor execution across types. For simple static field initialization, use a static field initializer instead of a static constructor because it is more readable and less verbose.

Primary Constructors in C# 12

C# 12 introduced primary constructors for classes and structs. Instead of declaring a constructor body separately, you place parameters directly on the type declaration. These parameters are in scope throughout the class, but they are not automatically stored as fields unless you use them in a field initializer or property initializer.

public class DistanceCalculator(double conversionFactor) { private readonly double _conversionFactor = conversionFactor; public double ConvertToMiles(double kilometers) { return kilometers * _conversionFactor; } } ```n In this example, `conversionFactor` is captured into a readonly field. If you do not use the parameter in any member initializer, it is only available during the constructor and is not retained. Primary constructors are most useful for small, data-centric types where the parameters map directly to properties or fields. They reduce boilerplate, but they can hide the difference between a constructor parameter and a captured field, so use them when the mapping is obvious. ## Constructor Injection for Testability Constructor injection is the most common way to supply dependencies to a class. Instead of creating dependencies inside the constructor, you accept them as parameters. This makes the dependencies explicit and allows you to swap implementations in tests. ```csharp public class OrderService { private readonly IPaymentGateway _paymentGateway; private readonly IOrderRepository _orderRepository; public OrderService(IPaymentGateway paymentGateway, IOrderRepository orderRepository) { _paymentGateway = paymentGateway ?? throw new ArgumentNullException(nameof(paymentGateway)); _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); } }

The null checks ensure the dependencies are provided at construction time. If a required dependency is missing, the failure happens immediately and clearly. This is preferable to discovering a null dependency later when the method that uses it is called.

Common Pitfalls and How to Avoid Them

One frequent mistake is doing too much work in the constructor. Constructors should initialize state, not perform long-running operations such as network calls or reading large files. Constructors are synchronous and cannot easily report failures after the object is partially built. If an operation can fail, it should be a method or a factory method that is explicitly called after construction.

Another pitfall is relying on virtual method calls inside a constructor. When a base class constructor calls a virtual method, the derived class's override executes before the derived class's own constructor has run. That override may access fields that are not yet initialized, leading to NullReferenceException. Avoid calling virtual members in constructors unless you fully understand the initialization order.

A third issue is using a constructor to enforce business rules that belong elsewhere. For example, a constructor that validates a state machine transition might be too restrictive if the object needs to be created in an intermediate state. Keep constructors for structural integrity, not complex domain logic.

Primitive Obsession and Long Parameter Lists

When a constructor takes many primitive parameters, the call sites become hard to read and easy to misorder. For example, a constructor like Point(double x, double y, double z) is clear, but Order(int customerId, int productId, int quantity, decimal price, string currency, DateTime created) is harder to use correctly.

In those cases, consider grouping related parameters into a parameter object. This reduces the parameter count and makes the relationship between values explicit.

public class Order { public Order(OrderDetails details) { Details = details; } public OrderDetails Details { get; } } public record OrderDetails(int CustomerId, int ProductId, int Quantity, decimal Price, string Currency);

This approach also makes it easier to evolve the API. Adding a field to OrderDetails does not change the constructor signature, so call sites that use the overloaded constructors may not need to change.

Constructor Usage in Dependency Injection Containers

Modern .NET applications often use a dependency injection (DI) container such as the built-in Microsoft.Extensions.DependencyInjection. The container examines the public constructor of a registered service and resolves its parameters by looking up the registered types. This is known as constructor injection.

When you have multiple public constructors, the container may become ambiguous. The built-in DI container picks the constructor with the most parameters that can be resolved. The behavior is not guaranteed across all containers, so it is safer to have a single public constructor. This also simplifies manual construction in unit tests and avoids surprising resolution errors.

Choosing Between Constructors and Factory Methods

Sometimes a constructor is not the best way to create an object. Factory methods can have descriptive names, return a more specific type, or implement complex creation logic that does not fit a constructor's constraints.

For example, a Connection class might have a private constructor and a public ConnectAsync method that returns a Task<Connection>. This pattern makes the asynchronous setup explicit and allows the class to control how instances are created.

public class DatabaseConnection { private DatabaseConnection(string connectionString) { ConnectionString = connectionString; } public string ConnectionString { get; } public static async Task<DatabaseConnection> CreateAsync(string connectionString) { var connection = new DatabaseConnection(connectionString); await connection.OpenAsync(); return connection; } }

Use a factory method when the initialization involves validation, asynchronous operations, or returning different implementations based on input. Use a constructor when you simply need to assign parameters to fields and perhaps perform a few simple checks.

Constructor Chaining and Base Class Initialization

Derived class constructors always call a base class constructor first. If you do not specify a base constructor, the parameterless base constructor is called implicitly. If the base class has only a parameterized constructor, the derived class must explicitly chain to it using : base(...).

public class BaseService { public BaseService(string connectionString) { ConnectionString = connectionString; } protected string ConnectionString { get; } } public class UserService : BaseService { public UserService(string connectionString, ILogger logger) : base(connectionString) { _logger = logger; } private readonly ILogger _logger; }

The chain ensures that the base class initializes its own state before the derived class adds anything. This is the correct order for most hierarchies. If you find yourself writing many layers of constructor chaining, consider whether deep inheritance is the right abstraction.

Runtime Cost and Allocation Considerations

Constructors themselves are not a major performance concern, but what you do inside them can be. Allocating large arrays, creating many temporary objects, or performing expensive computations during construction can show up in profiling. The most common performance issue is not the constructor itself but the repeated creation of objects that could be reused.

For example, creating a new Random instance in a constructor for each object that needs random numbers is a known anti-pattern. A static readonly Random shared across instances is better, but note the thread safety implications—Random is not thread-safe, so you might need a ThreadLocal<Random> instance. In general, keep constructors cheap and defer heavy work to lazily initialized properties or explicit setup methods when possible.

Another concern is the allocation of unnecessary objects during initialization. If a constructor initializes a list that is never used, or reads a configuration file that is always the same, you are paying a cost that could be avoided by making the operation lazy.

public class LazySettings { private Lazy<Settings> _settings = new Lazy<Settings>(LoadSettings); public Settings Current => _settings.Value; private static Settings LoadSettings() { // Load once, on first access } }

The Lazy<T> type delays the expensive operation until the property is first accessed, which can be a significant win if the object is created often but the settings are rarely read. This pattern is particularly useful in scenarios where objects are frequently constructed but only a subset of their features is used.

Thread Safety in Static Constructors

Static constructors are thread-safe by design. The runtime guarantees that a static constructor runs only once, even if multiple threads access the type simultaneously. This makes static constructors a safe place to initialize shared static state, as long as the initialization itself is thread-safe.

However, you should not use a static constructor to perform work that could deadlock. If a static constructor waits on a shared resource that another thread is holding, and that other thread is waiting for a type that has a static constructor running, a deadlock can occur. This is rare, but it is a real concern in complex applications. Prefer simple static field initializers or explicit static initialization methods that you call from the composition root.

Maintaining Readable and Maintainable Constructors

A constructor that takes many parameters is a sign that the class may be doing too much. When you see a constructor growing, ask whether the class has too many responsibilities. Splitting the class into smaller classes that each have a focused constructor often leads to more maintainable code.

Another maintainability tip is to use named arguments at the call site when a constructor has several parameters of the same type. This prevents errors from misplaced arguments and makes the call self-documenting.

var point = new Point( x: 10, y: 20, z: 30);

Named arguments also make it easier to skip optional parameters. However, they should not be used to hide a poorly designed constructor. If you find yourself always using named arguments to remember what each value means, consider introducing a parameter object instead.

Compatibility Across C# Versions

C# primary constructors are available only in C# 12 and later. If you are targeting an older language version, you cannot use that syntax. Similarly, some newer constructor features like required properties or init accessors may require a specific .NET version. Always check the language version and target framework before adopting newer syntax.

For compatibility, prefer the classic explicit constructor syntax when you are writing a library that may be consumed by projects using older C# versions. This is especially important for open-source libraries where users might be stuck on an older SDK for reasons outside your control.

Final Code Example: A Complete Constructor Pattern

The following example combines several patterns: a single public constructor, null checks, a private factory method for complex initialization, and the use of a parameter object to keep the constructor manageable.

public class PaymentProcessor { private readonly IPaymentGateway _gateway; private readonly PaymentConfiguration _config; public PaymentProcessor(IPaymentGateway gateway, PaymentConfiguration config) { _gateway = gateway ?? throw new ArgumentNullException(nameof(gateway)); _config = config ?? throw new ArgumentNullException(nameof(config)); } public static PaymentProcessor CreateDefault() { var config = LoadDefaultConfiguration(); return new PaymentProcessor(new DefaultGateway(), config); } private static PaymentConfiguration LoadDefaultConfiguration() { // Load from appsettings or environment } }

This design keeps the constructor simple, allows testers to inject custom dependencies, and provides a convenient factory method for application startup. The CreateDefault method centralizes the default wiring, so callers do not need to know the exact dependencies. This pattern is particularly useful in applications that use manual composition without a DI container.

c# constructor usage: Practical Usage and Code Examples | RYUSLOG DEV