Search code examples
c#rider

Does primary constructor in C# generates init only or protected set property?


Consider the following code:

public enum TransactionType{Foo,Bar}

public abstract record Transaction
{
    public TransactionType Type { get; }

    protected Transaction(TransactionType type)
    {
        Type = type;
    }
}

public record InheritedTransactionBarOnly: Transaction
{
    public InheritedTransactionBarOnly(): base(TransactionType.Bar){}
}

public static class Program
{
    public static void Main()
    {
        var iWantThis = new InheritedTransactionBarOnly();
        //var iDontWantThis = new InheritedTransactionBarOnly{Type = TransactionType.Foo}; // this shouldn't be possible
    }
}

Rider is suggesting to convert to a primary constructor: rider suggestion

but I do believe this is wrong, as this will allow for Type to be set during the initialization of the inherited record:

var inheritedTransaction = new InheritedTransactionBarOnly
{
   Type = TransactionType.Foo // This code compiles only after Rider suggested change, but not before
}

Is my understanding is wrong or it is just a bug in Rider?

P.S. This is what Transaction looks like after Rider's conversion:

public abstract record Transaction(TransactionType Type)
{
}

Solution

  • I made a few test cases to show the difference between how records are built. The following is just for class records, I'm not sure if the same is true for struct records.

    Starting with your example, SomeProperty { get; } this means the property can only be set during construction, not from new { SomeProperty = ... } code. But record primary constructor (the refactor suggestion) declares properties {get; init;} by default, which allows setting in new ... blocks. However, note that you can redefine properties inside the declaration; see the AltRecord record below. Perhaps this is more concise...

    See record reference in the section "If the generated auto-implemented property definition isn't what you want" ...


    So to answer your question, yes, this seems like a mistake in the refactor, because behavior is not the same. Here is some code to show the differences.

    public abstract record GetOnlyExample
    {
        // Readonly property (cannot set in new { SomeProperty = ... } )
        public int SomeProperty { get; }
    
        // Property can only be set once, from protected constructor.
        protected GetOnlyExample(int someProperty)
        {
            SomeProperty = someProperty;
        }
    }
    
    public record ConcreteGetOnlyExample : GetOnlyExample
    {
        // Concrete implementation, can only set SomeProperty through base constructor.
        public ConcreteGetOnlyExample() : base(1) { }
    }
    
    public abstract record GetInitExample
    {
        // Readonly property, allow in init (can set in new { SomeProperty = ... } )
        public int SomeProperty { get; init; }
    
        // Property can only be set during construction, or init.
        protected GetInitExample(int someProperty)
        {
            SomeProperty = someProperty;
        }
    }
    
    public record ConcreteGetInitExample : GetInitExample
    {
        // Concrete implementation, can only set SomeProperty during init/construction.
        public ConcreteGetInitExample() : base(1) { }
    }
    
    // Primary constructor, creates: public int SomeProperty { get; init; }
    public abstract record PrimaryConstructor(int SomeProperty)
    {
    }
    
    public record ConcretePrimaryConstructor : PrimaryConstructor
    {
        // Concrete implementation, can only set SomeProperty during init/construction.
        public ConcretePrimaryConstructor() : base(1) { }
    }
    
    // Primary constructor, creates: public int SomeProperty { get; init; }
    public abstract record AltRecord(int SomeProperty)
    {
        // redefine SomeProperty to be readonly (no init)
        public int SomeProperty { get; } = SomeProperty;
    }
    
    public record ConcreteAltRecord : AltRecord
    {
        public ConcreteAltRecord() : base(1) { }
    }
    
    internal class Program
    {
        static void Main(string[] args)
        {
            // compile error: not allowed during init:
            // ConcreteGetOnlyExample aaa = new ConcreteGetOnlyExample() { SomeProperty = 5 };
    
            // can set during init:
            ConcreteGetInitExample bbb = new ConcreteGetInitExample() { SomeProperty = 5 };
    
            // implicit init created through primary constructor:
            ConcretePrimaryConstructor ccc = new ConcretePrimaryConstructor() { SomeProperty = 8 };
    
            // compile error: SomeProperty was redefined as `get` only, can't set during init
            //ConcreteAltRecord ddd = new ConcreteAltRecord() { SomeProperty = 9 };
        }
    }