Record types

Record types are introduced in c# 9 which allows writing immutable reference types. The properties of an instance of reference type cannot change after its initialization.

public record User(string FirstName , string LastName);

var user = new User("Steve", "Jobs");

If we try to change the property value after object initialization we will get compiler error

user.FirstName = "Mark"; //Compiler error

RecordType.PNG

Non-destructive mutation using With

The only way to modify the properties of an immutable record instance that is already initialized is to create a new record instance and modify its properties at the moment of instantiation.

We can use “With” to specify only the properties we need to change when creating a new variable.

var user = new User("Steve", "Jobs");

var user2 = user with { LastName = "Smith" };

Similarly, we can even use with to copy an existing record:

var user = new User("Steve", "Jobs");
var user2 = user with { };

var isequal = user.Equals(user2);  // True

Inheritance

A record can inherit from another record.

public record User(string FirstName , string LastName);

public record PrimeUser(string FirstName , string LastName , string Email) : User(FirstName , LastName);

var Primeuser = new PrimeUser("Steve", "Jobs", "steve@jobs.com");

Record Type Internals - Compiler generated code

Record type is a compiler feature . The compiler will take the Record type and generate it us

public class User : IEquatable<User>
{
    [CompilerGenerated]
    private readonly string <FirstName>k__BackingField;

    [CompilerGenerated]
    private readonly string <LastName>k__BackingField;

    [System.Runtime.CompilerServices.Nullable(1)]
    protected virtual Type EqualityContract
    {
        [System.Runtime.CompilerServices.NullableContext(1)]
        [CompilerGenerated]
        get
        {
            return typeof(User);
        }
    }

    public string FirstName
    {
        [CompilerGenerated]
        get
        {
            return <FirstName>k__BackingField;
        }
        [CompilerGenerated]
        init
        {
            <FirstName>k__BackingField = value;
        }
    }

    public string LastName
    {
        [CompilerGenerated]
        get
        {
            return <LastName>k__BackingField;
        }
        [CompilerGenerated]
        init
        {
            <LastName>k__BackingField = value;
        }
    }

    public User(string FirstName, string LastName)
    {
        <FirstName>k__BackingField = FirstName;
        <LastName>k__BackingField = LastName;
        base..ctor();
    }

    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("User");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(" ");
        }
        stringBuilder.Append("}");
        return stringBuilder.ToString();
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append("FirstName");
        builder.Append(" = ");
        builder.Append((object)FirstName);
        builder.Append(", ");
        builder.Append("LastName");
        builder.Append(" = ");
        builder.Append((object)LastName);
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator !=(User left, User right)
    {
        return !(left == right);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public static bool operator ==(User left, User right)
    {
        if ((object)left != right)
        {
            if ((object)left != null)
            {
                return left.Equals(right);
            }
            return false;
        }
        return true;
    }

    public override int GetHashCode()
    {
        return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(<FirstName>k__BackingField)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(<LastName>k__BackingField);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public override bool Equals(object obj)
    {
        return Equals(obj as User);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    public virtual bool Equals(User other)
    {
        if ((object)this != other)
        {
            if ((object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(<FirstName>k__BackingField, other.<FirstName>k__BackingField))
            {
                return EqualityComparer<string>.Default.Equals(<LastName>k__BackingField, other.<LastName>k__BackingField);
            }
            return false;
        }
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(1)]
    public virtual User <Clone>$()
    {
        return new User(this);
    }

    protected User([System.Runtime.CompilerServices.Nullable(1)] User original)
    {
        <FirstName>k__BackingField = original.<FirstName>k__BackingField;
        <LastName>k__BackingField = original.<LastName>k__BackingField;
    }

    public void Deconstruct(out string FirstName, out string LastName)
    {
        FirstName = this.FirstName;
        LastName = this.LastName;
    }
}

In the above compiler generated code the overridden Equal and Hashcode implementation helps value based equality.

var user = new User("Steve", "Jobs");
var user2 = new User("Steve", "Jobs");
Console.WriteLine(user.Equals(user2)); // True

Despite being a class, a record type provides additional value-like behavior and semantics that differentiate it from a class. Records are reference types, but they have their own built-in equality check - the equality is checked by value rather than reference.