Photo by Eric Krull

Introduction

Whenever I code I prefer to have immutable domain models.
Example:

public class Game
{
    public Game(Team homeTeam, Team awayTeam, GameResult result)
    {
        HomeTeam = homeTeam ?? throw new ArgumentNullException(nameof(homeTeam));
        AwayTeam = awayTeam ?? throw new ArgumentNullException(nameof(awayTeam));
        Result = result ?? throw new ArgumentNullException(nameof(result));
    }

    public Team HomeTeam { get; }
    public Team AwayTeam { get; }
    public GameResult Result { get; }
}

Here you can see that it's only possible to set the properties when creating the object (get only properties).

By doing this you can make sure that you don't accidentally mutate the state of the object. This would not compile for example:

...
var game = new Game(homeTeam, awayTeam, result);
game.AwayTeam = new Team(Guid.NewGuid(), "Arsenal"); // Compilation error

Immutability comes with a lot of benefits, but sometimes it can be a bit cumbersome to deal with when you only want to update some properties. Since the object is immutable, you need to create a copy with all the existing values and the new updated one.

I will show you how Records in C# 9 will greatly simplify this

Let's set the scene

Let's say that we are building an application related to betting. A user can place bets on different games.

Our current domain model (before records) looks something like this (greatly simplified):

public class Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId {get; }
    public IReadOnlyCollection<BetRow> Rows { get; }
}

public class BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; }
}

A Bet contains a collection of BetRows and also some other data like Id and UserId. A BetRow also holds a BetRowOutcome property. This property will be updated when a game is finished.

public enum BetRowOutcome
{
    Unsettled,
    Win,
    Loss
}

Now we want to check if the user has won or not. We need to check and update the Outcome property for each BetRow.

Without records

I will now show you one of the "pain points" when it comes to immutability and c#. If we weren't using immutability, the following code would work. We just loop through the bet rows and update (mutate) the Outcome property.
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

        if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
        {
            betRow.Outcome = BetRowOutcome.Win;
        }
        else
        {
            betRow.Outcome = BetRowOutcome.Loss;
        }
    }

    return bet;
}

Since the above code doesn't compile, we need to add a bit more code to make it work.

public class BetService
{
    public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
    {
        var updatedBetRows = new List<BetRow>();
        foreach (var betRow in bet.Rows)
        {
            var result = finishedGames[betRow.GameId].Result;

            BetRowOutcome betRowOutcome;
            if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
            {
                betRowOutcome = BetRowOutcome.Win;
            }
            else
            {
                betRowOutcome = BetRowOutcome.Loss;
            }

            var updatedBetRow = new BetRow(betRow.GameId, betRow.HomeScore, betRow.AwayScore, betRowOutcome);
            updatedBetRows.Add(updatedBetRow);
        }

        return new Bet(bet.Id, bet.UserId, updatedBetRows);
    }
}
  1. We added a list that we use to keep track of the updated BetRows.
  2. Foreach row we create a new BetRow object and add it to the list.
  3. We return a new Bet with the updated rows.

This works great but imagine that your object has a bunch of different properties, you could easily end up with something like this:

return new Bet(
  bet.Id,
  bet.UserId,
  updatedBetRows,
  more,
  properties,
  here,
  updateThisAsWell,
  easy,
  to,
  forget,
  and,
  also,
  make,
  errors) 

Hopefully you get my point. :)
Basically we want to avoid passing all the existing parameters to the constructor.

With records

I have now converted the following classes to records.

public record Bet
{
    public Bet(string id, string userId, IReadOnlyCollection<Record.BetRow> rows)
    {
        Id = id ?? throw new ArgumentNullException(nameof(id));
        UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        Rows = rows ?? throw new ArgumentNullException(nameof(rows));
    }

    public string Id { get; }
    public string UserId { get; }
    public IReadOnlyCollection<Record.BetRow> Rows { get; init; }
}

public record BetRow
{
    public BetRow(string gameId, int homeScore, int awayScore) : this(gameId, homeScore, awayScore, BetRowOutcome.Unsettled)
    {
    }

    public BetRow(string gameId, int homeScore, int awayScore, BetRowOutcome outcome)
    {
        GameId = gameId ?? throw new ArgumentNullException(nameof(gameId));
        HomeScore = homeScore;
        AwayScore = awayScore;
        Outcome = outcome;
    }

    public string GameId { get; }
    public int HomeScore { get; }
    public int AwayScore { get; }
    public BetRowOutcome Outcome { get; init; }
}

The key thing here is that I changed class to record and added the init keyword to the Rows and Outcome properties. This allows me to do the following:
BetService

public Bet SettleBet(Dictionary<string, Game> finishedGames, Bet bet)
{
    var updatedBetRows = new List<Record.BetRow>();
    foreach (var betRow in bet.Rows)
    {
        var result = finishedGames[betRow.GameId].Result;

       BetRowOutcome betRowOutcome;
       if (betRow.HomeScore == result.HomeScore && betRow.AwayScore == result.AwayScore)
       {
           betRowOutcome = BetRowOutcome.Win;
       }
       else
       {
           betRowOutcome = BetRowOutcome.Loss;
       }

       var updatedBetRow = betRow with { Outcome = betRowOutcome }; // No more new
       updatedBetRows.Add(updatedBetRow);
    }
    return bet with { Rows = updatedBetRows }; // No more new
}

Now I can use the with keyword and only specify the properties that I would like to update. I will not mutate the existing Bet object, I will get a completely new object with updated Rows, all other properties will keep their old values.

This was just a quick example of how to use records, if you want to read more about records have a look here