Primary Constructors for All Types in C#

Primary Constructors for All Types in C#

In this article, I will discuss Primary Constructors for All Types in C# with Examples. Please read our previous article discussing C# 12 New Features Overview with Examples. With the release of C# 12, Primary Constructors have been expanded to all types, not just record types. This means that classes, structs, and records can now directly define constructor parameters inline within the type definition. This feature helps to simplify the code, reduce repetitive or standard code, and make the constructor declaration more concise. Previously, primary constructors were available only for record types, but now, class and struct types can also use this feature.

What Are Primary Constructors?

Primary Constructors were initially introduced with record types in C# 9. In C# 12, primary constructors allow a streamlined way to define parameters directly within the class or struct declaration. This simplifies the creation of objects and reduces the boilerplate code, particularly in scenarios where DTOs (Data Transfer Objects) or simple data-holding types are needed. The primary constructor syntax is now available for classes, structs, and records, whereas previously, it was only available for record types.

This feature reduces redundancy and improves the readability of your code, especially when you are defining classes or structs that primarily hold data.

Key Features of Primary Constructors for All Types
  • Simplified Constructor Syntax: Constructor parameters can be declared directly within the type declaration, eliminating the need for an explicit constructor definition.
  • Automatic Read-Only Fields: The parameters defined in the primary constructor are automatically treated as read-only fields inside the class or struct. You do not need to declare fields for these parameters manually.
  • Concise and Cleaner Code: The primary constructor significantly reduces repetitive code, making it easier to create and manage data classes and structs.
  • Available for Classes, Structs, and Records: In C# 12, primary constructors can now be used for classes, structs, and records, making this feature more widely applicable.
What’s New in C# 12?

Before C# 12, primary constructors were only available for record types. Starting from C# 12, you can also use primary constructors in classes and structs. This feature lets you define constructor parameters directly in the type declaration, making it easier to initialize object fields with minimal code.

Syntax: Without Primary Constructors
public class Person
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}
Syntax: With Primary Constructors

With C# 12, the constructor can be defined directly in the class declaration:

public class Person(string FirstName, string LastName)
{
    public string FullName => $"{FirstName} {LastName}";
}

In this case, the FirstName and LastName parameters are part of the constructor declaration itself. You don’t need to define them as fields or properties explicitly; they are automatically available for use within the class.

Example: Primary Constructor for Classes

Let’s start with a simple example of defining a class with a primary constructor.

namespace CSharp12NewFeatures
{
    // A class with a primary constructor in C# 12
    public class Product(int id, string name)
    {
        // The constructor parameters are automatically treated as readonly fields.
        public void Display()
        {
            Console.WriteLine($"Product ID: {id}, Product Name: {name}");
        }
    }

    public class Program
    {
        static void Main()
        {
            // Instantiate the class using the primary constructor
            var product = new Product(1, "Laptop");

            // Call the method to display product details
            product.Display();
        }
    }
}
Code Explanation:
  • Product Class: The Product class is defined by a primary constructor that accepts ID and name as parameters. The parameters id and name are automatically available as read-only fields within the class.
  • Using the Primary Constructor: The Product class is instantiated using the primary constructor (new Product(1, “Laptop”)). The Display() method uses these parameters (id and name) to display product details.
  • Simplified Constructor Declaration: There is no need to write an explicit constructor. The parameters are automatically injected into the class and are available for use.
Example: Primary Constructor for Structs

Let’s extend the primary constructor feature to structs and value types in C#.

namespace CSharp12NewFeatures
{
    // A struct with a primary constructor in C# 12
    public struct Point(int x, int y)
    {
        // The constructor parameters are automatically treated as readonly fields.
        public double DistanceFromOrigin() => Math.Sqrt(x * x + y * y);
    }

    public class Program
    {
        static void Main()
        {
            // Instantiate the struct using the primary constructor
            var point = new Point(3, 4);

            // Calculate and display the distance from the origin
            Console.WriteLine($"Distance from origin: {point.DistanceFromOrigin()}");
        }
    }
}
Code Explanation:
  • Point Struct: The Point struct has a primary constructor that accepts x and y as parameters. The parameters x and y are automatically available as read-only fields within the struct.
  • DistanceFromOrigin Method: The DistanceFromOrigin method calculates the Euclidean distance from the origin (0, 0) using x and y values.
  • Using the Primary Constructor: The struct is instantiated with new Point(3, 4), and the distance from the origin is calculated and displayed.
Real-Time Use Cases for Primary Constructors in C#
  • Data Transfer Objects (DTOs): Primary constructors can simplify the creation of DTOs (Data Transfer Objects) in applications where classes are primarily used for transporting data. Instead of writing constructors with many parameters, you can define the parameters in the primary constructor to simplify the creation and management of DTOs.
  • Simple Value Types: Structs, especially for value types that store coordinates or dimensions (e.g., Point, Rectangle), can benefit from primary constructors. This provides a concise way to create and initialize struct types that don’t require complex logic.
  • Immutable Entities: Classes or structs that need to be immutable can use primary constructors to automatically initialize read-only fields, providing a clear and succinct way to declare immutable types.
  • Configuration Settings: If you have classes for holding configuration settings that require multiple parameters, primary constructors can simplify their declaration and reduce boilerplate code.
Real-Time Scenario: Inventory Management System for E-Commerce

In this scenario, we have an e-commerce platform that manages a catalogue of products. Each product has properties like name, price, quantity in stock, and category. The goal is to use a primary constructor to streamline the creation of these Product objects, simplifying how we define product attributes and manage the inventory.

using System;
using System.Collections.Generic;

namespace CSharp12NewFeatures
{
    // Define the Product class with a primary constructor in C# 12
    public class Product(string name, decimal price, int stockQuantity, string category)
    {
        // The constructor parameters are automatically turned into readonly fields.

        // Additional methods can be added to perform business logic.
        public void UpdateStock(int quantitySold)
        {
            if (quantitySold > stockQuantity)
            {
                Console.WriteLine("Error: Not enough stock.");
            }
            else
            {
                stockQuantity -= quantitySold;
                Console.WriteLine($"Stock updated! {quantitySold} units sold.");
            }
        }

        // Method to get product details in a formatted string
        public string GetProductDetails()
        {
            return $"Product Name: {name}\nPrice: {price}\nStock Quantity: {stockQuantity}\nCategory: {category}";
        }
    }

    public class Program
    {
        static void Main()
        {
            // Create a list to hold products in the inventory
            List<Product> inventory = new List<Product>
            {
                new Product("Laptop", 999.99m, 50, "Electronics"),
                new Product("Smartphone", 699.99m, 200, "Electronics"),
                new Product("Desk Chair", 149.99m, 120, "Furniture")
            };

            // Print product details and update stock for the first product
            foreach (var product in inventory)
            {
                Console.WriteLine(product.GetProductDetails());
                product.UpdateStock(10);  // Assume 10 units of each product are sold
                Console.WriteLine();  // Add space between products for clarity
            }
        }
    }
}
Code Explanation:
Product Class with Primary Constructor:

The Product class is defined using a primary constructor that accepts the following parameters:

  • name (product name),
  • price (price of the product),
  • stockQuantity (quantity in stock),
  • category (product category).

These parameters are automatically available as read-only fields within the class.

Business Logic Methods:
  • UpdateStock method: This method reduces the product’s stock by the number of units sold. It also checks whether the stock is sufficient before allowing the sale.
  • GetProductDetails method: This method returns a string representation of the product’s details, including its name, price, stock quantity, and category.
Using Primary Constructor:
  • Products are created using the primary constructor directly, such as new Product(“Laptop”, 999.99m, 50, “Electronics”).
  • The parameters passed to the constructor (name, price, stockQuantity, category) are automatically treated as readonly fields within the class.
Inventory Management:
  • We create a list (inventory) to store multiple Product objects.
  • For each product in the inventory, we print its details using GetProductDetails() and then update its stock using the UpdateStock() method.
Output:

Primary Constructors for All Types in C#

Why This is a Real-Time Use Case
  • Simplified Class Definition: The primary constructor in C# 12 allows us to define the product’s attributes directly in the class definition, without needing a separate constructor. This simplifies the class declaration and reduces boilerplate code.
  • Automatic Read-Only Fields: The constructor parameters are automatically available as readonly fields in the class, making it easy to access these values within methods and business logic.
  • E-Commerce Application: This is a typical scenario for an inventory management system in an e-commerce platform. Products have attributes like name, price, stock quantity, and category, and these attributes are set at the time of object creation. By using the primary constructor, the process is streamlined, making the code more concise and easier to maintain.
  • Maintainability: As the number of products in your catalog grows, this pattern can be reused across different product types (e.g., physical products, digital products). It simplifies adding new product types without rewriting the constructor logic each time.
  • Business Logic Integration: With methods like UpdateStock, this example integrates business logic for handling stock updates directly within the class. The primary constructor makes the data initialization clean and self-contained.
Restrictions and Limitations of Primary Constructors in C# 12

Although primary constructors offer many benefits, they have certain limitations that should be considered.

  • No Access to Primary Constructor Parameters Outside the Type: Primary constructor parameters are automatically readonly fields inside the class or struct. However, they cannot be accessed directly outside the class unless they are explicitly exposed through methods or properties.
  • No Constructor Overloading: Constructor overloading is not supported with primary constructors. This means you cannot define multiple constructors with different parameter lists. All parameters must be defined in a single primary constructor declaration. Attempting to overload the constructor will result in a compile-time error.
  • No Mutability: The parameters defined in the primary constructor are readonly fields and cannot be modified after initialization. Once the object is created, the values of the constructor parameters cannot be changed. You can change the value within the class, but not from outside the class.
  • No Default Values for Constructor Parameters: Primary constructors do not allow default values for the constructor parameters. Every parameter must be explicitly passed when creating an instance of the class or struct.
Example to Understand the Primary Constructor Limitations:

The following example demonstrates the limitations of primary constructors in C# 12. This example covers all the restrictions:

  • No Access to Primary Constructor Parameters Outside the Type.
  • No Constructor Overloading.
  • No Mutability.
  • No Default Values for Constructor Parameters.

So, please modify the Program class as follows:

namespace CSharp12NewFeatures
{
    // A class with a primary constructor that defines name and price parameters
    public class Product(string name, decimal price)
    {
        // The parameters 'name' and 'price' are readonly fields within the class
        // They cannot be reassigned directly outside the class, but can be modified within methods.

        // Method to display product information
        public void DisplayInfo()
        {
            // Access the readonly fields inside the class
            Console.WriteLine($"Product: {name}, Price: {price}");
        }

        // Method to change the price (this is allowed within the class)
        public void AttemptChangePrice(decimal newPrice)
        {
            // Inside the class, we can modify the price directly since it's a readonly field
            price = newPrice;
            Console.WriteLine($"Price updated to: {price}");
        }

        // Attempting to overload the constructor will result in a compile-time error
        // public Product(string name) // Error: Constructor overloading not allowed with primary constructors
        // {
        //     this.name = name;
        //     this.price = 100.00m; // Attempt to assign default value to parameter will also result in error
        // }
    }

    class Program
    {
        static void Main()
        {
            try
            {
                // Instantiate the Product class using the primary constructor
                var product = new Product("Laptop", 999.99m);

                // Display product information
                product.DisplayInfo();

                // Attempt to change price (this works since it's inside the class)
                product.AttemptChangePrice(1099.99m); 

                // Display product information
                product.DisplayInfo();

                // You cannot change or access the price outside the class definition
                // product.price = 1199.99m;
            }
            catch (Exception ex)
            {
                // Catch and handle any exceptions
                Console.WriteLine($"ERROR: {ex.GetType().Name} – {ex.Message}");
            }
        }
    }
}
Key Points:
  • Constructor Parameters as Readonly Fields: The name and price parameters are readonly fields within the Product class. Readonly fields mean they cannot be reassigned outside the class after object creation.
  • Mutability Inside the Class: Within the class, these readonly fields can still be modified via instance methods like AttemptChangePrice(). This is perfectly valid, and the field value can be updated during the lifetime of the object, as demonstrated by modifying price.
  • No Reassignment Outside the Class: You cannot reassign price or name directly outside the class. Example: product.price = 1199.99m; would not be allowed and would throw a compile-time error because price is readonly outside the class.
Output:

Primary Constructors for All Types in C# with Examples

The introduction of primary constructors for all types in C# 12 significantly reduces the amount of repetitive or standard code required for defining constructors, especially for classes and structs. This feature enhances readability and simplicity by declaring parameters directly within the type definition, making the code more concise and easier to understand. Key benefits of primary constructors in C# 12 include:

  • Simplified syntax for declaring constructor parameters.
  • Automatic field creation for constructor parameters.
  • Improved code readability by reducing the need for explicit constructor definitions.

This feature is especially useful for DTOs, value types, and immutable entities, where the constructor simply initializes fields or properties without requiring complex logic.

In the next article, I will discuss Collection Expressions in C# with Examples. In this article, I explain Primary Constructors for All Types in C# with Examples. I want your feedback. Please post your feedback, questions, or comments about this article.

Leave a Reply

Your email address will not be published. Required fields are marked *