Getting Started with Primary Constructors in .NET 8 and C# 12

antonmartyniuk

Anton Martyniuk

Posted on April 19, 2024

Getting Started with Primary Constructors in .NET 8 and C# 12

Getting Started with Primary Constructors in .NET 8 and C# 12

Introduction

In .NET 8, C# 12 introduced a concise syntax for constructors —  primary constructors. These constructors are available for classes and structs and they look like primary constructors for records. But they have different behaviour.

This blog post will explain all the aspects of primary constructors, their nuances and difference when comparing to records.

How To Declare Primary Constructor in C

C# provides the following syntax for declaring a primary constructor for classes, structs and records:

public class ProductClass(int Id, string Name, string Description, decimal Price);

public struct ProductStruct(int Id, string Name, string Description, decimal Price);

public record ProductRecord(int Id, string Name, string Description, decimal Price);
Enter fullscreen mode Exit fullscreen mode

In records primary constructor parameters are stored as init only properties, while in classes  — parameters don’t become properties.

This means that when creating a new product — its properties won’t be available, as an opposite to a record.

var productClass = new ProductClass(1, "PC", "Some amazing PC", 1000.00m);

// Error: productClass.Name and other properties are not available here
Console.WriteLine(productClass.Name);

var productRecord = new ProductRecord(1, "PC", "Some amazing PC", 1000.00m);

// productRecord.Name is available here
Console.WriteLine(productRecord.Name);
Enter fullscreen mode Exit fullscreen mode

Moreover primary constructor parameters aren’t members of a class:

public class ProductClass(int Id, string Name, string Description, decimal Price)
{
    public override string ToString()
    {
        // Error: Name property is not a member of a class
        return this.Name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Class primary constructors serve a different purpose. They can be used:

  • to initialize a class property or a field
  • to reference constructor parameters in a class members
  • for dependency injection

How To Initialize Class Properties Using Primary Constructors

First let’s have a look on how to declare a Product class with a classic constructor:

public class Product
{
    public int Id { get; }
    public string Name { get; }
    public string Description { get; }
    public decimal Price { get; }

    public Product(int id, string name, string description, decimal price)
    {
        Id = id;
        Name = name;
        Description = description;
        Price = price;
    }
}
Enter fullscreen mode Exit fullscreen mode

Primary constructors offer a much more concise way to assign class properties:

public class Product(int id, string name, string description, decimal price)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
    public string Description { get; } = description;
    public decimal Price { get; } = price;
}

var product = new Product(1, "PC", "Some amazing PC", 1000.00m);
Console.WriteLine(product.Name);
Enter fullscreen mode Exit fullscreen mode

Here we are utilizing primary constructor parameters to assign properties of the Product class. Did you notice that parameters now have a camelCase naming? It is a recommended naming style for primary constructors for classes and structs.

It differs from PascalCase naming for records, because in classes primary constructor parameters are not properties as they are in records. That’s why C# team has chosen a different naming style to visually distinguish these constructor parameters.

How To Initialize Class Fields Using Primary Constructors

Primary constructor parameters can be assigned to a class fields too:

public class Product(int id, string name, string description, decimal price)
{
    private readonly int _id = id;

    public string Name { get; } = name;
    public string Description { get; } = description;
    public decimal Price { get; } = price;
}
Enter fullscreen mode Exit fullscreen mode

How To Use Primary Constructor Parameters Inside Class Members

Primary constructor parameters can be freely used inside any of a class members. For example, we can use these parameters inside a ToString method:

public class Product(int id, string name, string description, decimal price)
{
    public override string ToString()
    {
        return $"Id: {id}; Name: {name}; Description: {description}; Price: {price:00.00} $";
    }
}
Enter fullscreen mode Exit fullscreen mode

If primary constructor parameters are assigned to properties, fields or are used inside methods: the compiler creates a storage for them in the giving type. Otherwise, the primary constructor parameters aren’t stored in the object.

How To Use Primary Constructors For Dependency Injection

Let’s explore an example of a ProductsService class that has few dependencies injected via constructor:

public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    private readonly IProductPriceCalculator _priceCalculator;

    public ProductService(
        IProductRepository productRepository,
        IProductPriceCalculator priceCalculator)
    {
        _productRepository = productRepository;
        _priceCalculator = priceCalculator;
    }

    public Product GetById(int id)
    {
        return _productRepository.GetById(id);
    }

    public Product Create(Product product)
    {
        product.Price = _priceCalculator.Calculate(product);

        product = _productRepository.Create(product);

        return product;
    }
}
Enter fullscreen mode Exit fullscreen mode

With a help of a primary constructor you can make your ProductService more concise:

public class ProductService(
    IProductRepository productRepository,
    IProductPriceCalculator priceCalculator) : IProductService
{
    public Product GetById(int id)
    {
        return productRepository.GetById(id);
    }

    public Product Create(Product product)
    {
        product.Price = priceCalculator.Calculate(product);

        product = productRepository.Create(product);

        return product;
    }
}
Enter fullscreen mode Exit fullscreen mode

Important tip: all class dependencies that are used in primary constructors are freely mutable and aren’t readonly or init only as in the 1st example. Make sure to consider this behavior when deciding whether to use primary constructors for Dependency Injection or not.

There are debates in the dev community: some like to use primary constructors for dependencies, some does not. This is a kind of tradeoff — on one hand you have more concise code, but less safer, on the other hand — you have safety, but more code. Select what works for you the best.

Mutable State of Primary Constructor Parameters

As mentioned above — primary constructor parameters are freely mutable.

For example, a MutableProduct class that updates the primary constructor parameter in the ApplyDiscount method:

public class MutableProduct(int id, string name, string description, decimal price)
{
    public int Id { get; } = id;
    public string Name { get; } = name;
    public string Description { get; } = description;
    public decimal Price => price;

    public void ApplyDiscount(decimal discountPercentage)
    {
        price -= price * (discountPercentage / 100);
    }
}
Enter fullscreen mode Exit fullscreen mode

When accessing a Price property, it's value will be updated after calling the ApplyDiscount method.

var mutableProduct = new MutableProduct(1, "PC", "Some amazing PC", 1000.00m);

// Outputs: 1000,00
Console.WriteLine(mutableProduct.Price);

mutableProduct.ApplyDiscount(20);

// Outputs: 800,00
Console.WriteLine(mutableProduct.Price);
Enter fullscreen mode Exit fullscreen mode

How To Initialize Base Class When Using Primary Constructors

Imagine you want to sell digital products, like an online PDF book. Such a digital product will have the same properties as a Product class with additional FileSize (in kilobytes) and DownloadLink properties.

We can define a DigitalProduct class that inherits from a Product class. When declaring a child class you have 2 options to call constructor of the base class.

The 1st option is to use a parent’s primary constructor and call it directly at a definition level:

public class DigitalProduct(
    int id, string name, string description, decimal price,
    double fileSize, string downloadLink)
    : Product(id, name, description, price)
{
    public double FileSize { get; } = fileSize;
    public string DownloadLink { get; } = downloadLink;
}
Enter fullscreen mode Exit fullscreen mode

The 2nd option is to use a regular constructor and call a base() method to initialize the parent object:

public class DigitalProduct : Product
{
    public DigitalProduct(
        int id, string name, string description, decimal price,
        double fileSize, string downloadLink)
        : base(id, name, description, price)
    {
        FileSize = fileSize;
        DownloadLink = downloadLink;
    }

    public double FileSize { get; }
    public string DownloadLink { get; }
}
Enter fullscreen mode Exit fullscreen mode

There is one caveat when using derived classes with primary constructors. Let’s add a ToString method to the DigitalProduct class:

public class DigitalProduct(
    int id, string name, string description, decimal price,
    double fileSize, string downloadLink)
    : Product(id, name, description, price)
{
    public double FileSize { get; } = fileSize;
    public string DownloadLink { get; } = downloadLink;

    public override string ToString()
    {
        return $"Id: {id}; Name: {name}; Description: {description}; Price: {price:00.00} $";
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example we are using primary constructor parameters inside a ToString method. The problem with this approach is that two copies of parameters are being created: one in the base class and another in the derived class. So when accessing data inside a DigitalProduct and Product classes - data may be out of sync.

The better option is to always use the Properties in the ToString method instead of constructor parameters:

public class DigitalProduct(
    int id, string name, string description, decimal price,
    double fileSize, string downloadLink)
    : Product(id, name, description, price)
{
    public double FileSize { get; } = fileSize;
    public string DownloadLink { get; } = downloadLink;

    public override string ToString()
    {
        return $"Id: {Id}; Name: {Name}; Description: {Description}; Price: {Price:00.00} $";
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Primary constructors introduced in C# 12, offer a much more concise way to assign class properties. While the primary constructors in classes and records may look the same, but they serve a different purpose.

It is important to remember the following nuances:

  • in records primary constructor parameters are stored as init only properties, while in classes  — parameters don’t become properties
  • class primary constructor parameters are not readonly or init only, they are freely mutable
  • class primary constructor parameters are stored in the object if they are assigned to properties, fields or used inside class methods

Class primary constructors can be used to:

  • to initialize a class property or a field
  • to reference constructor parameters in class members
  • for dependency injection

Hope you find this blog post useful. Happy coding!

Originally published at https://antondevtips.com.

After reading the post consider the following:

  • Subscribe to receive newsletters with the latest blog posts
  • Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)

If you like my content —  consider supporting me

Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!

💖 💪 🙅 🚩
antonmartyniuk
Anton Martyniuk

Posted on April 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related