General purpose immutable classes in C#

For larger types I will build a With function that has arguments that all default to null if not provided:

public sealed class A
{
    public readonly X X;
    public readonly Y Y;

    public A(X x, Y y)
    {
        X = x;
        Y = y;
    }

    public A With(X X = null, Y Y = null) =>
        new A(
            X ?? this.X,
            Y ?? this.Y
        );
}

Then use the named arguments feature of C# thus:

val = val.With(X: x);

val = val.With(Y: y);

val = val.With(X: x, Y: y);

I find int a much more attractive approach than lots of setter methods. It does mean that null becomes an unusable value, but if you're going the functional route then I assume you're trying to avoid null too and use options.

If you have value-types/structs as members then make them Nullable in the With, for example:

public sealed class A
{
    public readonly int X;
    public readonly int Y;

    public A(int x, int y)
    {
        X = x;
        Y = y;
    }

    public A With(int? X = null, int? Y = null) =>
        new A(
            X ?? this.X,
            Y ?? this.Y
        );
}

Note however, this doesn't come for free, there are N null comparison operations per call to With where N is the number of arguments. I personally find the convenience worth the cost (which ultimately is negligible), however if you have anything that's particularly performance sensitive then you should fall back to bespoke setter methods.

If you find the tedium of writing the With function too much, then you can use my open-source C# functional programming library: language-ext. The above can be done like so:

[With]
public partial class A
{
    public readonly int X;
    public readonly int Y;

    public A(int x, int y)
    {
        X = x;
        Y = y;
    }
}

You must include the LanguageExt.Core and LanguageExt.CodeGen in your project. The LanguageExt.CodeGen doesn't need to included with the final release of your project.

The final bit of convenience comes with the [Record] attribute:

[Record]
public partial class A
{
    public readonly int X;
    public readonly int Y;
}

It will build the With function, as well as your constructor, deconstructor, structural equality, structural ordering, lenses, GetHashCode implementation, ToString implementation, and serialisation/deserialisation.

Here's an overview of all of the Code-Gen features


For this exact case I am using Object. MemberwiseClone(). The approach works for direct property updates only (because of a shallow cloning).

sealed class A 
{
    // added private setters for approach to work
    public X x { get; private set;} 
    public Y y { get; private set;} 

    public class A(X x, Y y) 
    { 
        this.x = x; 
        this.y = y; 
    } 

    private A With(Action<A> update) 
    {
        var clone = (A)MemberwiseClone();
        update(clone);
        return clone;
    } 

    public A SetX(X nextX) 
    { 
        return With(a => a.x = nextX); 
    } 

    public A SetY(Y nextY) 
    { 
        return With(a => a.y = nextY); 
    } 
 }