Primary Constructors & Required Members in C# 12
Master C# 12 primary constructors and required members with practical examples.
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
- ✓Basic knowledge of C# classes and constructors
- ✓Familiarity with object initializers
- ✓Understanding of properties and fields
- ✓Visual Studio 2022 or .NET 8 SDK installed
- Primary constructors allow you to declare constructor parameters directly in the class declaration, reducing boilerplate.
- Required members enforce that certain properties or fields are set during object initialization.
- Both features work together to create immutable, self-documenting types.
- Primary constructors are available for classes, structs, and records.
- Required members can be used with object initializers and are validated at compile time.
Imagine you're ordering a custom pizza. The primary constructor is like saying 'I want a pizza with cheese and pepperoni' right when you order, instead of first saying 'I want a pizza' and then adding toppings later. Required members are like mandatory fields on a form – you can't submit without filling them in. Together, they make sure your pizza is exactly what you wanted, with no missing ingredients.
C# 12 introduces two powerful features that streamline object creation and enforce initialization contracts: primary constructors and required members. These features are part of the ongoing effort to make C# more expressive and less verbose, especially when working with immutable data and dependency injection.
Primary constructors allow you to declare constructor parameters directly in the class declaration, eliminating the need for explicit constructor bodies and field assignments. This is particularly useful for simple types that primarily hold data or have straightforward initialization logic. Required members, on the other hand, ensure that certain properties or fields are set during object initialization, either via constructor parameters or object initializers. This prevents the common bug of forgetting to initialize a critical member.
Together, they enable a more declarative style of programming where the structure of your class communicates its initialization requirements. In this tutorial, we'll explore both features in depth, with practical examples, common pitfalls, and production-ready patterns. By the end, you'll be able to write cleaner, safer C# code that leverages the full power of .NET 8.
Understanding Primary Constructors
Primary constructors allow you to declare constructor parameters directly in the class declaration. This feature is available for classes, structs, and records. The parameters are in scope throughout the class body, meaning you can use them to initialize fields, properties, or even pass to base constructors.
Consider a simple Person class without primary constructors:
```csharp public class Person { public string FirstName { get; } public string LastName { get; }
public Person(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } } ```
With primary constructors, the same class becomes:
``csharp public class Person(string firstName, string lastName) { public string FirstName { get; } = firstName; public string LastName { get; } = lastName; } ``
The parameters firstName and lastName are available throughout the class. They are captured and can be used in property initializers, method bodies, etc. However, they are not automatically stored as fields; you must explicitly assign them if you want to keep them.
Primary constructors are especially useful for simple data holders and types that require dependency injection. They reduce boilerplate and make the class's dependencies explicit.
Required Members: Enforcing Initialization
Required members are a new feature that allows you to mark properties or fields as mandatory for initialization. This means that any code creating an instance of the class must set those members, either via constructor or object initializer. If a required member is not set, the compiler emits an error.
To declare a required member, use the required modifier on a property or field. Required members can be used in classes, structs, and records. They work well with object initializers and constructors.
Example:
```csharp public class Product { public required string Name { get; init; } public required decimal Price { get; init; } public string? Description { get; init; } }
// Usage var product = new Product { Name = "Laptop", Price = 999.99m }; // Description is optional ```
If you forget to set Name or Price, you'll get a compilation error. This is a powerful way to enforce initialization contracts without writing custom constructors.
Required members can also be combined with primary constructors. For example:
``csharp public class Order(int orderId) : IOrder { public required int OrderId { get; init; } = orderId; public required DateTime OrderDate { get; init; } } ``
Here, OrderId is set from the primary constructor parameter, but OrderDate must be set via object initializer.
Combining Primary Constructors and Required Members
Primary constructors and required members can be used together to create concise yet robust initialization patterns. The primary constructor parameters can be used to set required members, and additional required members can be set via object initializers.
Consider a scenario where you have a base class with a primary constructor and a derived class that adds required members:
```csharp public class Person(string name) { public required string Name { get; init; } = name; }
public class Employee(string name, int id) : Person(name) { public required int Id { get; init; } = id; }
// Usage var emp = new Employee("Alice", 1); // Both Name and Id are set via primary constructor parameters ```
In this example, the primary constructor parameter name is used to initialize the required property Name. The derived class's primary constructor passes name to the base class and also initializes Id. This pattern ensures that all required members are set without needing explicit constructor bodies.
However, there is a nuance: when you use a primary constructor parameter to initialize a required member, you must ensure that the parameter is captured. In the above example, name is captured because it's used in the property initializer. If you don't use it, you'll get a warning.
Another pattern is to have required members that are not set by the primary constructor but must be set via object initializer. This is useful when you have some mandatory parameters and some optional ones that are still required.
Example:
```csharp public class Order(int orderId) { public required int OrderId { get; init; } = orderId; public required DateTime OrderDate { get; init; } }
// Usage var order = new Order(123) { OrderDate = DateTime.UtcNow }; ```
Here, OrderId is set from the constructor, but OrderDate must be set in the object initializer.
Required Members in Interfaces and Abstract Classes
Required members can also be used in interfaces and abstract classes to enforce that implementing or derived classes provide certain properties. This is a powerful way to define contracts that include initialization requirements.
For interfaces, you can declare required properties without a body. Any class implementing the interface must then provide those required members.
Example:
```csharp public interface IConfigurable { required string ConnectionString { get; init; } required int TimeoutSeconds { get; init; } }
public class DatabaseConfig : IConfigurable { public required string ConnectionString { get; init; } public required int TimeoutSeconds { get; init; } }
// Usage var config = new DatabaseConfig { ConnectionString = "Server=...", TimeoutSeconds = 30 }; ```
In abstract classes, you can mark abstract or virtual properties as required. Derived classes must then override them and mark them as required as well.
Example:
```csharp public abstract class Vehicle { public abstract required string Model { get; init; } public abstract required int Year { get; init; } }
public class Car : Vehicle { public override required string Model { get; init; } public override required int Year { get; init; } } ```
Note that when overriding a required member, the override must also be marked as required. This ensures that the initialization contract is preserved.
Common Pitfalls and Best Practices
While primary constructors and required members are powerful, they come with some pitfalls. Understanding these will help you avoid bugs and write cleaner code.
Pitfall 1: Accidentally Capturing Parameters
Primary constructor parameters are captured if used in the class body. If you don't intend to store them, be careful not to use them inadvertently. For example:
``csharp public class Logger(string category) { public void Log(string message) { Console.WriteLine($"[{category}] {message}"); // Captures category } } ``
Here, category is captured because it's used in the Log method. If you only need it for initialization, consider using a local variable or a field.
Pitfall 2: Mutable Primary Constructor Parameters
If you capture a primary constructor parameter in a mutable field, you can change it later, which might break immutability. Prefer capturing in readonly fields or init-only properties.
Pitfall 3: Required Members with Default Values
Required members cannot have default values because they must be explicitly set. If you need a default, consider making the member optional (not required) and providing a default in the constructor.
Best Practices:
- Use primary constructors for simple types that primarily hold data.
- Use required members for properties that are essential for the object's validity.
- Combine them for immutable types with clear initialization contracts.
- Avoid using primary constructors for classes with complex initialization logic; use explicit constructors instead.
- Document required members with XML comments to inform consumers.
Real-World Example: Configuration Object
Let's build a real-world configuration object that uses both primary constructors and required members. This is a common scenario in .NET applications where you need to load settings from appsettings.json and enforce that all required settings are present.
Consider an application that connects to a database and an external API. The configuration class might look like this:
``csharp public class AppConfig(string environment) { public required string Environment { get; init; } = environment; public required string DatabaseConnectionString { get; init; } public required string ApiEndpoint { get; init; } public int TimeoutSeconds { get; init; } = 30; } ``
Here, Environment is set from the primary constructor parameter, but DatabaseConnectionString and ApiEndpoint are required and must be set via object initializer. TimeoutSeconds has a default value and is optional.
When loading from configuration, you can use the options pattern:
```csharp // In Program.cs var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<AppConfig>(options => { var env = builder.Environment.EnvironmentName; // Create instance with primary constructor and object initializer var config = new AppConfig(env) { DatabaseConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"), ApiEndpoint = builder.Configuration["ApiEndpoint"] }; // Copy to options options.Environment = config.Environment; options.DatabaseConnectionString = config.DatabaseConnectionString; options.ApiEndpoint = config.ApiEndpoint; options.TimeoutSeconds = config.TimeoutSeconds; }); ```
This pattern ensures that all required configuration values are provided at startup. If any are missing, you'll get a compile-time error if you use the required members correctly.
Alternatively, you can use the [Required] data annotation, but that only works at runtime. Required members give you compile-time safety.
The Missing Initialization Bug
- Use required members for properties that must be set during initialization.
- Avoid relying on implicit initialization; be explicit about what's required.
- Leverage compiler checks to catch missing initializations early.
- Document initialization contracts with required members.
- Consider using primary constructors to reduce boilerplate and improve clarity.
Add 'X = value;' in constructor or use 'new MyClass { X = value }'Check if property is marked 'required' and you are using an object initializer.| File | Command / Code | Purpose |
|---|---|---|
| PrimaryConstructorExample.cs | public class Person(string firstName, string lastName) | Understanding Primary Constructors |
| RequiredMemberExample.cs | public class Employee | Required Members |
| CombinedExample.cs | public class Product(string name, decimal price) | Combining Primary Constructors and Required Members |
| InterfaceRequiredExample.cs | public interface IEntity | Required Members in Interfaces and Abstract Classes |
| PitfallExample.cs | public class BadExample(string name) | Common Pitfalls and Best Practices |
| AppConfigExample.cs | public class AppConfig(string environment) | Real-World Example |
Key takeaways
Common mistakes to avoid
3 patternsForgetting to capture a primary constructor parameter in a field or property.
Using a mutable property to capture a primary constructor parameter, breaking immutability.
Not marking a property as required when it must be set for the object to be valid.
Interview Questions on This Topic
Explain the difference between a primary constructor and an explicit constructor in C# 12.
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
That's C# Advanced. Mark it forged?
6 min read · try the examples if you haven't