Refactor your C# code with primary constructors

David Pine

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:

40 comments

Leave a comment

  • Stilgar Naib 5

    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

    public record Person
    (
        string Name,
        string Title,
        int Age
    );

    Since we’re defining a type more so than writing a constructor it makes more sense to make it look like a type definition

  • Andrew Witte 1

    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 data-model devs.
    IMO a lot of actual needed improvements to the C# lang get held back by legacy IL models. Really just need to break compatibility with old libs and failed ideas of the past. To bad this wasn’t done when .NET Core started as it was the perfect time to do so when ppl were willing to port stuff. This choice will forever haunt future improvements I think that could have been. O well now I guess.

    • DiegoSynth 0

      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).

  • Edison Henrique Andreassy 4

    Please, don’t make another way to do the same thing. Primary constructors are not good.

  • Jane Malico 1

    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

  • Jorn Vanloofsvelt 4

    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 the ability to make them read-only, and I agree that the syntax feels a bit awkward. Nevertheless, I trust that the team behind this language feature carefully considered feedback during the design phase. Since I have not given my feedback then I won’t be complaining here.

    • Hamed 2

      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.

  • DiegoSynth 2

    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.

  • Louis Lecailliez 3

    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.

  • neon-sunset 0

    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 Rust style of neatly grouping types in Contracts.cs and Services.cs – works really well for lean microservices.

    Thank you.

Feedback usabilla icon