I was roped into a discussion about how the Visual Studio debugger interprets parameters passed via a primary constructor and I thought it was worth a quick explanation.

C# 12 introduced primary constructors, they provide a really convenient syntax to declare constructors with parameters that are available anywhere in the body of the type. This means primary constructor parameters are in scope throughout the class or record definition. However, it's important to view primary constructor parameters as parameters. Take this simple command line example with a record type that has two constructor parameters:

var dist1 = new Distance(3, 4);

Debugger.Break();

public record Distance(double dx, double dy)
{
    public double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
    public  double Direction { get; } = Math.Atan2(dy, dx);
}

We have defined dx and dy as doubles and both parameters become available to be accessed throughout the definition of the Distance record type which includes its properties. What is interesting about record types is that the parameters are also in my locals window when I stop at the Debugger.Break() line, it almost looks like they are properties, just like Magnitude and Direction. My locals windows looks like this:

Visual Studio locals view of the record

Now let’s say, for whatever reason, we rewrite that record to be a class, and we convert the Direction property into a method:

var dist1 = new Distance(3, 4);
var dir = dist1.Direction();

Debugger.Break();

public class Distance(double dx, double dy)
{
    public double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
    public double Direction()
    {
        return Math.Atan2(dy, dx);
    }
}

With a class though the parameters from a primary constructor are strictly available within the body of the class and its public and private methods. So in the Distance class example only the Magnitude property is visible in the Locals window. In order to see the parameters from the primary constructor while debugging, I would need to step through the Direction method or property getter in Distance class. Your locals now look like this when you are paused on the Debugger.Break() line.

Visual Studio locals view of the class

This difference between records and classes makes sense to me given how we use records, but I get how it can be confusing. Do you think this is the right way for the Visual Studio debugger to handle this? Would you want to see the parameters passed in through the primary constructor in the Locals window for classes too? You can vote on my preferred suggestion here.

A vinyl record with a question mark in the middle


Comment Section