C# 12 primary constructors

A concise syntax to declare constructors whose params are available anywhere in the body.

Primary constructors is an easier way to create a constructor for your class or struct, by eliminating the need for explicit declarations of private fields and bodies that only assign param values.

They are great when you just need to do simple initialization of fields and for dependency injection.

Code example without primary constructors

public class BookDefault
{
	public int Id { get; }
	public string Title { get; }
	public int Pages { get; set; }
	private readonly List<decimal> ratings = new List<decimal>();
	public decimal? AverageRating => ratings.Any() ? ratings.Average() : 0m;

	public BookDefault(int id, string title, IEnumerable<decimal>? rating = null)
	{
		ArgumentException.ThrowIfNullOrEmpty(id);
		ArgumentException.ThrowIfNullOrEmpty(title);
		
		Id = id;
		Title = title;
		if(rating?.Any() == true)
		{
			ratings.AddRange(rating);
		}
	}
}

Code example using primary constructors

public class Book(int id, string title, IEnumerable<decimal> ratings)
{
	public int Id => id;
	public string Title => title.Trim();
	public int Pages { get; set; }
	public decimal AverageRating => ratings.Any() ? ratings.Average() : 0m;
}

another example

public class Person(string firstName, string lastName)
{
	public string FirstName { get; } = firstName;
	public string LastName { get; } = lastName;
}

example with validation on initialization

public class Person(string firstName, string lastName)
{
	private readonly string _firstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
	private readonly string _lastName = IsValidName(lastName) ? lastName : throw new ArgumentNullException(nameof(lastName));
	
	public string FullName => $"{_firstName} {_lastName}";
	
	private static bool IsValidName(string name)
		=> name is not "";
}

we can also have multiple constructors, but it’s always necessary to use this() initializer. This ensures that the primary constructor is always called and all necessary data to create the class is present.

public class Book(int id, string title, IEnumerable<decimal> ratings)
{
	// new other constructors
	public Book(int id, string title) : this(id, title, Enumerable.Empty<decimal>()) {}

	public Book() : this(99, "Demo book") {}

	// properties
	public int Id => id;
	public string Title => title.Trim();
	public int Pages { get; set; }
	public decimal AverageRating => ratings.Any() ? ratings.Average() : 0m;
}

Primary constructors with DI

Code example before

public class BookService
{
	private readonly IBookRepository _bookRepository;

	public BookService(IBookRepository bookRespository)
	{
		_bookRepository = bookRespository;
	}

	public async Task<IEnumerable<Book>> GetAll()
	{
		return await _bookRepository.GetAll();
	}
}

Code example using primary constructors

public class BookService(IBookRepository bookRepository)
{
	public async Task<IEnumerable<Book>> GetAll()
	{
		return await bookRepository.GetAll();
	}
}

They’re mutable! (can’t be readonly)

(!) There’s a caveat. Primary constructors are not completely equivalent if you have defined your fields as readonly, because primary constructor params are mutable.
If you want to maintain the readonly behavior, use a field declaration in place. (!)

public class BookService(IBookRepository bookRepository)
{
	// immutable field declaration
	private readonly IBookRepository _bookRepository = bookRepository;
	
	public async Task<IEnumerable<Book>> GetAll()
	{
		return await _bookRepository.GetAll();
	}
}

Reference(s)

https://henriquesd.medium.com/net-8-and-c-12-primary-constructors-cd498d21bd08
https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors
https://devblogs.microsoft.com/dotnet/csharp-primary-constructors-refactoring/
https://andrewlock.net/an-introduction-to-primary-constructors-in-csharp-12/