Buildable Immutable Types (a C# proposal)

In 2016 I submitted a C# proposal for Buildable Immutable Types to the Roslyn team. The feedback I got from other community members was that my proposal would work better as a T4 Template or as part of the "upcoming Source Generation" feature.

Now, 4 years later, with Source Generators finally coming to C# I thought it would be the right time to follow up on it.

The initial replies I received on GitHub were a little disappointing, but considering that the same people who poo-pooed the suggestions also created this issue 2 years later suggests that the core idea is good (especially considering how welcomed their proposal was). This seems to support my concern that dealing with the construction of immutable types is a painful process in C#.

I have reproduced the original proposal below. Next time, I'll describe my experiencing developing the source generator.

The proposal, as originally written

Like the rest of you, I try to create immutable types where possible. Consider the simple Person class below:

public class Person
{
    public Person(string name)
    {
        Name = name;
        // ... set other properties
    }

    public string Name { get; }
    // ... define properties
}

As the number of fields grow, the parameters start to get quite out of control. Now consider that the properties of Person must be calculated from a variety of sources. In our build method we can either have variables all over the place, or we must create a new PersonBuilder class which stores properties and instantiates the Person class.

// Creates a person based on some fancy logic
public Person CreatePersonUsingVariables(...)
{
    var name = ...;

    // complex computations of various properties
    // this logic can have branches which set different properties
    // or might call to methods.

    return new Person(name, ...);
}


// Creates a person based on a builder class
public Person CreatePersonUsingBuilder(...)
{
    var builder = new PersonBuilder();

    builder.Name = ...;
    // set properties when needed, can pass builder around to other methods

    return builder.Build();
}

// Builder (have to define manually)
public class PersonBuilder
{
    public string Name { get; set; }
    // ... define properties

    public Person Build() => new Person(name, ...);
}

The drawbacks of creating your own builder class are related to maintainability and overhead. Many times developers will just forego immutability in order to save time having to defines these builder types.

My proposal is for C# to automatically create a builder class. Syntax would look something like this:

// C# code
public buildable class Person
{
    string Name { get; }
}

// Compiler generates
public class Person
{
    public Person(string name)
    {
        Name = name;
        // ... set other properties
    }

    public string Name { get; }
    // ... define properties

    public class Builder
    {
        public string Name { get; set; }
        // ... define properties

        public Person Build() => new Person(name, ...);
    }
}

Things to note:

  • The modifier buildable lets the compiler know how to treat this.

  • Properties on buildable types are implicitly public. Like interfaces no modifier is allowed.

  • The compiler adds a public nested class within the buildable type called "Builder".

  • In all other ways the compiler can treat Person and Person.Builder as regular types.

  • We can add methods to the Person type. If methods on Person.Builder are needed, the following syntax can be used:

    // C# code
    public buildable class Person
    {
        string Name { get; }
    
        // Methods applicable to Person
        public void Eat()
        { 
            // In this scope, properties are readonly
        }
    
        // I can choose to override GetHashCode() and Equals(object)
    
        builder // Defines a scope for methods of Person.Builder
        {
            public void CalculateAge(DateTime dob) {
                // In this scope, properties are mutable
            }
        }
    }
    
  • We can also have a method which generates the builder from the type.

    // Compiler generated, for brevity I have excluded code from previous listing
    public class Person
    {       
        public Builder GetBuilder()
        {
            return new Builder
            {
                Name = name,
                // ... other properties
            };
        }
    }
    
  • Optional: We can extend this to buildable structs


Other posts you might like


Join the Discussion

You must be signed in to comment.

No user information will be stored on our site until you comment.