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

  • Melissa P 29

    I don’t use primary constructors, it just “compacts” code in a “wrong” way.

    For records, structs it’s fine, it makes those definitions more compact. But for a class with methods, it doesn’t save any code lines and it makes everything so much harder to read. Plus, the totally failed design to not have the auto-generated fields and properties readonly makes it fully unusable. Yes, you can bypass that as shown in the example by creating shadow properties with anti-collision underscore prefixed clone properties… right… but all you create is an even bigger pile of a mess and the complete opposite of clean code.

    • Mads TorgersenMicrosoft employee 7

      The “totally failed design” to make primary constructor parameters mutable was a difficult one for us to land on. On the one hand, you’d clearly often want that in this particular situation. On the other hand, every other parameter everywhere in C# is mutable (which I’m no fan of – it’s definitely on my list of things I would go back and change if I had a time machine!), so it would be highly inconsistent to make an exception here.

      In the end we decided to stick with consistency. At the same time, we were – and are – open to adding a readonly annotation to primary constructor parameters. It’s more complicated than it sounds (Can a readonly primary constructor parameter be mutated during the initialization of the object? Should other parameters be able to be declared readonly?) and can be added separately, so we decided to wait for user feedback from primary constructors in the wild before settling this part of the design.

      This comment section certainly has such user feedback! 😉

      • Andrew Mansell 3

        I can’t see any reason why readonly parameters should be mutable during initalization, that would be confusingly different from readonly fields. I also don’t see any (immediate?) need to allow method parameters to be readonly. But for me, readonly primary constructor parameterss are an absolute pre-requesite for the entire feature to be worth using.

      • Simon Ziegler 0

        It’s not a failed design in the context of how C# has been developed. I’ve been wondering for a while about a new language, let’s say C## for reference. C## could borrow concepts from elsewhere including what I’ve recently seen as I explore Rust. Default immutability. Classes default sealed. Spans everywhere. Greater use of functional programming concepts (LINQ is amazing), and perhaps Rust style enums. Basically everything that will enforce reliability, maintainability and performance.

        • Marcel Bradea 0

          .NET definitely could badly use a C# successor, and the best successor for this is likely to adopt Swift.

          All these ancient un-changable decisions have piled up heavily over the years. From nullable as a true first-class citizen (as it is in Swift), to no semi-colons and no parentheses around if/switch/catch statements, read-only by default for everything (params and variables alike – ie: `let` VS `var` Swift keyword, having types be defined after a colon (ie: `let variable:SomeType` as they are in Swipt/Kotlin/TypeScript), to trailing lambda block expressions that can enable nested syntax such as Kotlin’s Compose and Swif’s SwiftUI to replace XML-based XAML (best example of the innate expressivity limitations of of the C# language).

          .NET/CLR was supposed to be built upon enabling this exact type of innovation, yet here we are with Swift and Kotlin having been around for over a decade now…. we are WAY past due.

          Pride aside, you guys should really look at reaching out to Silver and pursuing an acquisition of their Swift for .NET implementation as you did for Xamarin. Adopting it for .NET as a first-class citizen would be a dream – and likely a much better use of resources than iterating on C#. @madst

          Just look at how Google adopted both Kotlin and Jetbrain’s IDE’s as the best-in-class tools. A strategy like this on .NET with Swift and Rider as first-class citizens (cross-platform and crucially also on Mac) would be the dream decade ahead for .NET development. Imagine the boost to ASP.NET and by association Azure that this could boom for Microsoft (particularly due to the once-in-history association with OpenAI and the generative-AI media floodlight it has brought to the company). Announcing a future of cloud-first .NET development on Swift as the future gold standard for back-end development would pull swathes of developers from other ecosystems (Rails, Python, Node etc.), and Microsoft would be a much stronger competitor to push innovation forward than Apple since MS is a leader on cloud while Apple is just a consumer of it. #boldvision

    • Alex Yumashev 7

      Exactly.

      Ironically, the “non-refactored” code is more readable: I can tell that _logger is a private field just by looking at its name (underscore). Now it’s indistinguishable from a local var.

      P.S. This might sound rude (and I apologize for that) but I really wish MS put their focus on fixing tons of bugs and issues with .NET, VS and MAUI instead of this… I’ve sent tons of VS “feedback items” with broken syntax highlight in VS – none of them fixed. I also submitted a github issue that it took them 5 years to fix https://github.com/dotnet/aspnetcore/issues/41340 MAUI still has no built-in support for biometrics (no fingerprint/faceid – which is kinda embarrassing for a mobile framework) but MS management rather focus on inventing features no one asked for.

    • Hamed 0

      I think it’s maybe a matter of context, if you have a 100+ lines of code class can say it has no value, but if you have a 20 lines class, say welcome to less 7 lines of codes 😀

    • BellarmineHead 4

      I was struggling to define what I didn’t like about the look of primary constructors, and this heads the nail on the head:-

      it just “compacts” code in a “wrong” way.

      Could/should it have been done in a better way? I doubt it. “Reducing boilerplate” isn’t always a good or necessary or justified thing, and certainly not when it introduces non-trivial amounts of “cognitive load”… (“huh? what is this doing?”).

      PCs seem to have introduced another mouth to feed in the household, one that I didn’t want and it seems many didn’t want. It’s now going to suck up time, thought, problems, discussion, fixes, etc. etc. … and we didn’t even need it anyway!

      Worse, it diverts time, attention and effort from what (it seems) people actually DO want (including me), which is Discriminated Unions.

    • zig zag 0

      At first I found primary constructors to be kind of meh due to various limitations. But then, I’ve tried the FaustVX.PrimaryParameter.SG package and this turned them into a very nice feature.

      The article should really mention the FaustVX.PrimaryParameter.SG package. Maybe in another blog post?

      public partial class Worker(
          [Field] ILogger<Worker> logger,
          [Property] IFoo foo
      ) : 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 Foo.DoSomeMoreWorkAsync(stoppingToken);
              }
          }
      }
  • Jarosław Jasiński 8

    Primary constructors are not functions for Visual Studio. You can’t use “Find All References Shift+F12” to find references to primary constructors.

    • Moaz Alkharfan 2

      Yes, this needs to be fixed.
      This ^ and no modifiers is why i don’t use these constructors.

  • Matvei Stefarov 20

    I am not a fan of primary constructors. Sure they make the code a little shorter, but also harder to read. The syntax made sense for records, which are inherently simple, but on fully-featured classes it’s just an unnecessary syntax sugar. Why have inconsistent syntax between your primary/secondary constructor? Why have inconsistent naming between your constructor-supplied and other fields? To save 3 lines of code? I wish this syntax bloat didn’t make it out of the proposal stage.

  • Jack Bond 3

    Microsoft’s guidance is async all the way down, but that’s impossible because async constructors have not been implemented. Are they necessary? Absolutely, as demonstrated by Microsoft / Azure’s own SDKs. Why do I mention this? Because the team responsible for C# decided to implement primary constructors:

    THE SINGLE DUMBEST FEATURE ADDED SINCE V1.

    NOBODY NEEDED THE EFFING THINGS.

    EFFING NOBODY.

    • Rob Walker 1

      Yet an async constructor goes against the last 20+ years of c# development. Making async constructor could potentially lead to people doing way too many things that shouldn’t be in a constructor.

      • Jack Bond 0

        Are you serious?

        There’s ABSOLUTELY NOTHING stopping developers from using .Result or Wait() in a constructor.

        And what exactly in 20 years of development goes against an async constructor? Developers can do absolutely tons of horrible stuff virtually anywhere in their code, but here this one place Microsoft is going to hold the line? By not offering async constructors, developers inevitably end up using .Result or Wait() BECAUSE Microsoft has left this glaring hole. Or they’re forced to do other horrific workarounds.

  • deadpin 16

    There are many good ideas inside C#. Primary constructors are NOT one of them.

    What a mess.

  • Karl 6

    Probably the worst feature I’ve ever seen implemented in C#, maybe even worse than the range syntax. Easy to read and understand has always been the best part of c#. This goes completely against that.

    • Andrew Turrentine 1

      I really like the range and index syntax upgrades. They are just straight up useful. I wouldn’t compare these two 😛

  • Mike Dean 4

    Things you didn’t know you needed…because you didn’t need them. That’s 5 minutes of my time I’ll never get back but at least I didn’t waste as much time reading about this useless feature as the C# team wasted implementing it.

    Increasingly, I think they implement what’s easy, not what’s needed.

  • Michael Taylor 4

    While I’m sure primary constructors have their uses for some people I personally have never seen a use for them. When teaching students about C# it just makes the syntax harder to understand when you actually get to constructors (which many types will need anyway) AND if the students accidentally put a method call signature on the type it will now see it as a primary constructor. It just confuses things at the benefit of a few keystrokes. We forbid the use of primary constructors in our coding guidelines.

    When I first saw this feature I thought about Typescript’s private constructor stuff and I do use that. But this isn’t that at all. The fact that the field isn’t readonly strikes me as odd. I can see reasons why you wouldn’t but honestly most ctor parameters are readonly anyway. Another thing that seems just broken is the fact that the example of how to make the field “readonly” doesn’t really do that. All it does, from what I can tell, is create yet another field that is readonly and assign the constructor parameter to it. Provided everyone understands this then they MAY use the readonly field but more likely there will be mixed uses of both fields which just wastes space and defeats the purpose.

    Overall this feature was half baked when it was implemented. I even remember conversations around it being half baked when it was being discussed but it got thrown in anyway. This just goes back to my feeling that C# became a hodge podge language (like C++) of “this would be cool features” extracted from popular languages at the time. A small group of OSS folks forced the feature in to save themselves time and now we all suffer the added complexity of the language. C# –> C++ more and more every release…

    • Andrew Turrentine 1

      From my experience, it helps with teaching developers that came from TypeScript or Kotlin background that are moving to a .NET project. So I guess it depends on the teaching environment as I am mostly dealing with people in a corporate environment that already have a good foundation in other languages and frameworks and not academic teaching.

      I agree with the rest though. They really need a readonly option built into the setup for primary constructors.

    • Mads TorgersenMicrosoft employee 1

      As to the concern about accidentally getting two fields, let me add a bit of clarity about the feature design:

      • A primary constructor parameter only leads to the generation of a backing field if it is “captured” by being used after initialization
      • The compiler generates a warning if a primary constructor is BOTH captured AND used to initialize a field or property

      This combination means that if you declare the field manually (e.g. to make it readonly) and initialize it from the primary constructor parameter, then you won’t also have accidental use (and hence capture) of the parameter itself.

      A similar warning applies if a primary constructor parameter is captured and also passed to the base constructor, for similar reasons of avoiding accidental duplication.

      • Andrew Mansell 0

        It still appears in Intellisense though, generally muddying the waters.

        Having to manually assign to a readonly field removes almost all benefit to primary constructors, as such we’ve set up our shared .editorconfig files to stop suggesting them.

        Please add the ability to declare readonly on the primary constructor parameter, it’s the 80% (at least) case.

      • Jack Bond 1

        When there are so many issues with async initialization, how could the C# team possibly justify implementing this prior to async constructors?

        Without them, how many times have developers resorted to using Wait() in a constructor?

        How many time has this happened?

        public async Task SomeMethodAsync(CancellationToken cancellationToken)
        {
        if(!Initialized)
        {
        await InitializeAsync(); // Not sure if the object is ready, better check everywhere.
        }
        }

        Please stop building features which will supposedly make it easier for developers to learn C# and actually implement features which existing C# developers need.

        • Chris Warrick 2

          If you need to do something async at class initialization, make the constructor private, and have a public static method that creates an instance, does the asynchronous stuff, and returns the instance.

          • Jack Bond 1

            “and have a public static method that creates an instance”

            Yep, and everyone rolls their own. How exactly does that seamlessly integrate with Dependency Injection libraries? Async constructors would offer a standard way of doing what you described, and inevitably work with DI libraries. Other than perhaps properties, async is supported EVERYWHERE, why not constructors?

      • Andrew Turrentine 0

        Mads, why can’t we just have modifiers there like readonly in the primary constructor though? It would simplify everything that you said and make it easier to understand. Personally, I do what you said and like primary constructors overall, but it does feel like it is missing the syntactical sugar for readonly since it is a common and recommended scenario.

  • Andrew Turrentine 2

    I like the idea and overall syntax of primary constructors. It is nice having similar syntax as TypeScript and Kotlin for helping transition between or from other languages.

    However, I think it does need a built-in readonly syntax. If it was a new language then readonly by default would be ideal, although I think that may be confusing since you have to specify readonly in other parts of .NET so for consistency I wish we could add readonly (and maybe other modifiers).

    • Andrew Witte 0

      Ya in a new lang “public readonly” by default would be best.
      And readonly in this context means its still mutable in a private use. Then you just use the public keyword to remove readonly mutability.
      Not only would this have made code much faster be default, it would have simplified a lot of syntax stuff as well.

  • Ben 1

    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.

    public int A = 3;
    public int B => 3;

Feedback usabilla icon