C# 12 as part of .NET 8 introduced a compelling set of new features! In this post, we explore one of these features, specifically primary constructors, explaining its usage and relevance. We’ll then demonstrate a sample refactoring to show how it can be applied in your code, discussing the benefits and potential pitfalls. This will help you understand the impact of the change and help influence your adoption of the feature.
Primary Constructors 1️⃣
Primary constructors are considered an “Everyday C#” developer feature. They allow you to define a class
or struct
along with its constructor in a single concise declaration. This can help you reduce the amount of boilerplate code you need to write. If you’ve been following along with C# versions, you’re likely familiar with record
types, which included the first examples of primary constructors.
Differentiating from record
types
Record types were introduced as a type modifier of class
or struct
that simplifies syntax for building simple classes like data containers. Records can include a primary constructor. This constructor not only generates a backing field but also exposes a public property for each parameter. Unlike traditional class
or struct
types, where primary constructor parameters are accessible throughout the class definition, records are designed to be transparent data containers. They inherently support value-based equality, aligning with their intended role as data holders. Consequently, it’s logical for their primary constructor parameters to be accessible as properties.
Refactoring example✨
.NET provides many templates, and if you’ve ever created a Worker Service, you’ve likely seen the following Worker
class template code:
namespace Example.Worker.Service
{
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
}
The preceding code is a simple Worker
service that logs a message every second. Currently, the Worker
class has a constructor that requires an ILogger<Worker>
instance as a parameter and assigns it to a readonly
field of the same type. This type information is in two places, in the definition of the constructor, but also on the field itself. This is a common pattern in C# code, but it can be simplified with primary constructors.
It’s worth mentioning that the refactoring tooling for this specific feature isn’t available in Visual Studio Code, but you can still refactor to primary constructors manually. To refactor this code using primary constructors in Visual Studio, you can use the Use primary constructor (and remove fields)
refactoring option. Right-click on the Worker
constructor, select Quick Actions and Refactorings...
(or press Ctrl + .), and choose Use primary constructor (and remove fields)
.
Consider the following video demonstrating Use primary constructor refactoring functionality:
The resulting code now resembles the following C# code:
namespace Example.Worker.Service
{
public class Worker(ILogger<Worker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
}
That’s it, you’ve successfully refactored the Worker
class to use a primary constructor! The ILogger<Worker>
field has been removed, and the constructor has been replaced with a primary constructor. This makes the code more concise and easier to read. The logger
instance is now available throughout the class (as it’s in scope), without the need for a separate field declaration.
Additional considerations 🤔
Primary constructors can remove your hand-written field declarations that were assigned in the constructor, but with a caveat. They’re not entirely functionally equivalent if you have defined your fields as readonly
because primary constructor parameters for non-record types are mutable. So, when you’re using this refactoring approach, be aware that you’re changing the semantics of your code. If you want to maintain the readonly
behavior, use a field declaration in place and assign the field using the primary constructor parameter:
namespace Example.Worker.Service;
public class Worker(ILogger<Worker> logger) : BackgroundService
{
private readonly ILogger<Worker> _logger = logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
Additional constructors 🆕
When you define a primary constructor, you can still define additional constructors. These constructors are required, however; to call the primary constructor. Calling the primary constructor ensures that the primary constructor parameters are initialized everywhere in the class declaration. If you need to define additional constructors, you must call the primary constructor using the this
keyword.
namespace Example.Worker.Service
{
// Primary constructor
public class Worker(ILogger<Worker> logger) : BackgroundService
{
private readonly int _delayDuration = 1_000;
// Secondary constructor, calling the primary constructor
public Worker(ILogger<Worker> logger, int delayDuration) : this(logger)
{
_delayDuration = delayDuration;
}
// Omitted for brevity...
}
}
Additional constructors aren’t always needed. Let’s do some bonus refactoring to include a few other features!
Bonus refactoring 🎉
Primary constructors are awesome, but there’s more we can do to improve the code.
C# includes file-scoped namespaces. They’re a really nice feature that reduces a level of nesting and improves readability. Continuing with the previous example, place your cursor at the end of the namespace name, and press the ; key (this isn’t supported in Visual Studio Code, but again you can do this manually). This will convert the namespace to a file-scoped namespace.
Consider the following video demonstrating this functionality:
With a few additional edits, the final refactored code is as follows:
namespace Example.Worker.Service;
public sealed class Worker(ILogger<Worker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
if (logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1_000, stoppingToken);
}
}
}
In addition to refactoring to file-scoped namespaces, I also added the sealed
modifier, as there’s a performance benefit in multiple situations. Finally, I’ve also updated the numeric literal passed into the Task.Delay
using the digit separator feature, to improve the readability. Did you know there’s a lot more to simplify your code? Check out What’s new in C# to learn more!
Next steps 🚀
Try this out in your own code! Look for opportunities to refactor your code to use primary constructors and see how it can simplify your codebase. If you’re using Visual Studio, check out the refactoring tooling. If you’re using Visual Studio Code, you can still refactor manually. To learn more, explore the following resources:
So many questions here…
What about null-checks? (Since you still could receive a null object in runtime, unless it is a value-type, even you declared parameter as non-nullable)
How about code coverage for this? We still have coverage issues with records, although they are with us for 3 years already.
Where should I put a breakpoint to verify I’m receiving a correct value in runtime?
Although, I do like that C# is constantly evolving, some features like this one...
It seems there is a lot of negative feedback stemming from wanting for PCs to do more than just being a shorthand ctor declaration. This is sad and seems to be in line with loaded expectations towards inline null-checks ( keyword) that C# had to backtrack on.
I, for one, am happy they were introduced as they allow me to be faster at modelling application state and make it look terser, closer to F# or perhaps...
That has to be the most useless and confusing C# features of all time, that absolutely nobody asked for. Please send the design team touch grass.
The code example from the blog post itself demonstrates extraordinarily well where the issue lies: two lines of code saved (4 if counting braces) at the cost of explicitness when reading, and decision making when writing code. This is a complete disaster.
They just keep on adding garbage that nobody asked for and “simplifies” nothing. These features are just bad design examples.
Microsoft, do you want to add something? Grow some balls and add multiple inheritance, and quit toying around with stupidness. Come on big guy, you don’t have what it takes? Then quit butchering C# and go to play with your Power Tools.
I believe some of the strong comments here are unnecessary. Personally, I find the range syntax to be excellent (somebody else here had criticism on that as well). As for the primary constructors, the concept is promising. I’ve often wished for something akin to TypeScript’s approach, where constructor arguments automatically become fields or properties, eliminating the need for reassignment within the constructor. It seems this feature aims to address that gap. However, I do miss...
totally agreed, I think primary constructor may not be suitable for all cases, yet I have used them in cases that make my code much cleaner.
This example is not good not only because of all the reasons connected to primary constructors mentioned in the previous comments. Even logging is messed up. If someone want a high performance logging, one should use proper log actions instead of cluttering code with IsEnabled check.
https://learn.microsoft.com/en-us/dotnet/core/extensions/high-performance-logging
https://github.com/dotnet/runtime/issues/45290#issue-752502603
Please, don’t make another way to do the same thing. Primary constructors are not good.
Just talking but IMO, Primary constructors are largely just a hack around the flawed property model instead of a better field access modifier system. Never did like the idea of hiding methods (aka properties) behind field like syntax. This has been abused and confused so many people over the years.
And records are a big mistake in C# I think. A class type should have just been expanded to support features of records for ASP.NET...
I’ve found myself forced to apply bad designs uncountable times because of the lack of multiple inheritance.
There’s no way around it. You just end up composing things because you cannot properly inherit, and it destroys the design.
That’s something they should have implemented ages ago (just to name ONE).
No. Just no. Already banned them in my editorconfig. I want to use properties not fields and I don't like how everything gest crammed together especially with more parameters. The class name gets separated from the related types it inherits and implements by things that are not related in the same way.
On a side note could you please make the default formatters play well or even suggest positional records formatting like that
<code>
Since we're defining a...
Besides what others have already said, I also have issues with how classes and records treat primary constructors differently. One makes private fields, and the other makes public properties. This is a random confusing piece of knowledge that you just have to know to use these effectively.
Though, I also find the one character difference between fields and properties to also be a bit weird.
<code>