.NET Tools How-To's

Primary Constructors – Using C# 12 in Rider and ReSharper

ReSharper and Rider support for C# 12

Welcome to our series, where we take a closer look at the C# 12 language features and how ReSharper and Rider make it easy for you to adopt them in your codebase. If you haven’t yet, download the latest .NET 8 SDK and update your project files!

In this series, we are looking at:

This is the first post, so let’s dive into primary constructors.

Background & Syntax

If we look at our codebases, we will realize that many of our classes and structs only define a single constructor to receive inputs and perform trivial initializations or pass values to the base constructor. Declaring these constructors involves tedious and boilerplate syntax, like access modifiers, repeated type names, and more braces. In C# 9, we saw a terse syntax for primary constructors in record declarations. With the release of C# 12, we finally get primary constructors for regular classes and structs (non-records) in our developer toolbox.

Unlike primary constructors for records – which implicitly declare properties corresponding to constructor parameters – primary constructors in regular classes/structs will not automatically expose their parameters. They also won’t get auto-generated members for equality, deconstruction, and cloning. It’s more common for regular classes/structs to encapsulate data into private fields instead of exposing it publicly.

Here’s a simple example of a class using primary constructors:

public class Derived(int i, string s, bool b) : Base(s)
{
    public int I { get; set; } = i;
    public string B => b.ToString();
}

public class Base(string s); // no body required

Now let’s unfold what happens to our class parameters:

  • string s is passed on to our base type
  • int i is used to initialize an auto-property
  • bool b is the first parameter where the magic happens…

Primary constructor parameters are scoped to the whole class. That means any instance member – except secondary constructors – can access them. The C# compiler makes this happen by inferring the set of private fields required for us and capturing the corresponding value in those fields. It’s worth noting that a capture only occurs if the parameter is accessed from an instance member.

In low-level C#, our example compiles as follows:

public class Derived : Base
{
    [CompilerGenerated] private bool <b>P;

    [CompilerGenerated] private int <I>k__BackingField;

    public int I
    {
        [CompilerGenerated] get => <I>k__BackingField;
        [CompilerGenerated] set => <I>k__BackingField = value;
    }

    public string B => <b>P.ToString();

    public Derived(string s, int i, bool b)
    {
        <b>P = b;
        <I>k__BackingField = i;
        base..ctor(s);
    }
}

Try Rider’s IL Viewer to see the magic happening in your own code!

Now that we’ve dealt with the basics, let’s see how ReSharper and Rider help you work with primary constructors! We will focus primarily (pun intended) on new features and less on existing features that have been updated for primary constructors (including syntax parsing, refactorings, etc.).

Conversion & Simplification

As with every new C# syntax feature, .NET developers face the challenge to adapt. Not only would we like to know if something can be written more elegantly, but also we might like to convert all our existing code to the new and shiny. Well, ReSharper and Rider definitely got you covered there!

If we take our example from above written with an explicit constructor, you will see a suggestion to convert to primary constructor. Did we rewrite that example manually? No, there’s also a context action to convert to explicit constructor!

Converting between primary and explicit constructors
Converting between primary and explicit constructors

If you fall for an old habit and assign parameters to fields, ReSharper and Rider will help you to replace field usages with primary constructor parameters (or vice versa if you actually want it):

Replacing field usages with primary constructor parameters
Replacing field usages with primary constructor parameters

As with plenty of our inspections, remember that there’s a way to apply them in bulk on any scope you like. If you want to simplify constructors in your whole solution, you can do this effortlessly:

Converting to primary constructors in the whole solution
Converting to primary constructors in the whole solution

Closing the chapter on simplifications for primary constructors, you can also remove redundant constructors and bodies with a quick-fix easily:

Removing empty redundant primary constructors and bodies
Removing empty redundant primary constructors and bodies

Double Capture Warnings

We already talked about capturing, but what exactly is double capturing, and why is it bad? Let’s consider the following example:

public class Person(int age)
{
    // initialization
    public int Age { get; set; } = age;

    // capture
    public string Bio => $"My age is {age}!";
}

In this class, the parameter age is exposed both through the Age and Bio property. As a result, the object stores the state of age twice! For reference types, a double capture leads to an increased memory footprint and possibly even memory leaks. In our concrete example, you will observe the following unintended behavior:

var p = new Person(42);
p.Age.Dump();   // Output: 42
p.Bio.Dump();   // Output: My age is 42!

p.Age++;
p.Age.Dump();   // Output: 43
p.Bio.Dump();   // Output: My age is 42! // !!!!

ReSharper and Rider notify you about potential double-capture issues through an inspection:

Double capturing inspection

And, of course, there is also a quick-fix to replace captures with property access:

Fixing double capture with property usage
Fixing double capture with property usage

Unnecessary captures can also occur in more involved situations, for example, in type hierarchies where the derived type captures it while it could use a property from the base type. In this case, the quick-fix will suggest to use the initialized property from the base type:

Fixing double capture with base-type property usage
Fixing double capture with base-type property usage

Of course, there might be no property in the base type just yet. ReSharper and Rider will still let you know so that you can take action and extract the captured parameter into a property.

Semantic Highlighting

Personally, I’m a big fan of how ReSharper and Rider augment your code with more information through visual cues like colors and inlay hints. For primary constructors, in particular, we thought it would be great to see whether a parameter is captured at a glance. In our initial example, you will see that b is slightly blue:

Highlighting for captured primary constructor parameters
Highlighting for captured primary constructor parameters

If the difference is too subtle for you, you can tweak your color scheme under:

  • In Rider, under Settings | Editor | Color Scheme | C# | Properties and variables | Primary Constructor Parameter
  • In ReSharper, under Options | Environment | Fonts and Colors | Display items | ReSharper C# Primary Constructor Parameter

Code Style & Formatting

Every team wants their code to be styled and formatted a bit differently. ReSharper and Rider help you solve this task automatically through the Reformat Code action! As part of our primary constructor support, we’ve added a couple new code style settings under Tabs, Indents, Alignment, and Line Breaks and Wrapping specifically for primary constructors:

Tabs, Indents, Alignment settings for primary constructors
Tabs, Indents, Alignment settings for primary constructors
Line Breaks and Wrapping settings for primary constructors
Line Breaks and Wrapping settings for primary constructors

You can try these new settings by selecting a code block and invoking the Configure code style action. Alternatively, you can also grab the following snippet and adapt the formatter comments:

// @formatter:wrap_primary_constructor_parameters_style chop_always // wrap_if_long | chop_always
// @formatter:max_primary_constructor_parameters_on_line 3
// @formatter:indent_primary_constructor_decl_pars inside // inside | outside_and_inside | outside | none
// @formatter:keep_existing_primary_constructor_declaration_parens_arrangement true
// @formatter:wrap_before_primary_constructor_declaration_rpar false
// @formatter:wrap_before_primary_constructor_declaration_lpar false

file class Formatting(int number, string text, bool boolean);

Conclusion

In this post, we discovered a lot of features related to the new C# 12 primary constructors. Try ReSharper 2023.3 EAP or Rider 2023.3 EAP now – not only to improve your productivity but also to save yourself from nasty bugs. If you see any opportunities for additional support, please let us know in the comments below! As always, thank you for reading.

Image credit: Dollar Gill

image description