How to write Records with MVUX

What is a Record

A record behaves like a class*, offering the feature of immutability, where the values assigned to it remain unchanged once set. It's possible to create records using the record modifier, for example:

public record MyRecord();

Records can be, but are not necessarily, immutable. When creating a record, it's important to use the keyword init instead of set for properties. If you choose set, the record won't remain immutable. Consequently, you won't be able to prevent values from changing once they're set. Additionally, ensure that sub properties or objects also remain immutable to maintain the integrity of the entire structure.

Also, records introduce the with operator, which is a helpful tool to deal with immutable objects, we will see more about this operator in the Updating records section.

Important

* When using record struct, there are some differences in how it behaves compared to regular records or classes because it combines value-type characteristics with the features of records. Learn more about struct.

Why Immutability in MVUX

Immutability is crucial in MVUX for two main reasons:

Predictable State Changes

Immutability ensures that once we set a state, it can't be changed. This predictability makes it easier to understand how our application behaves over time. In MVUX, we use immutable data structures to represent the application state.

Concurrency and Threading

Immutability makes our application more robust in handling concurrency and threading challenges. Immutable data structures are naturally thread-safe, reducing the risk of bugs related to multiple things happening simultaneously in our app.

How to create immutable records

You can create immutable records in three ways. First, declare your record with a primary constructor and parameters; this will create an immutable record with the specified parameters as its properties:

public partial record ChatResponse(string Message, bool IsError);

The second way is by creating properties using the init keyword instead of set to enforce immutability. Here's a brief example:

public partial record ChatResponse
{
    public string Message { get; init; }
    public bool IsError { get; init; }
}

Finally, it's possible to mix the two approaches by using the primary constructor and adding properties for non-essential values. For example, consider a ChatResponse record where Message is essential, but IsError is not:

public partial record ChatResponse(string Message)
{
    public bool IsError { get; init; }
}
new ChatResponse("Hello, I'm a bot"); //with IsError defaulting to false

How to use records with MVUX

Records are designed to be a simple data structure, excellent for exchanging data, such as requests and responses, between application layers.

For instance, in our ChatService, the AskAsync method is called from the Model. It receives a list of ChatEntry records, which are used to create a request. The method returns a ChatResponse (record) instance to the Model, handling data from the presentation layer to the business layer:

public async ValueTask<ChatResponse> AskAsync(IImmutableList<ChatEntry> history)
{
    var request = CreateRequest(history);

    var result = await _client.CreateCompletion(request);

    if (result.Successful)
    {
        var response = result.Choices.Select(choice => choice.Message.Content);

        return new ChatResponse(string.Join("", response));
    }
    else
    {
        return new ChatResponse(result.Error?.Message, IsError: true);
    }
}

Updating records

As we are dealing with immutable records, it's not possible to update them or their properties. To achieve that, we need to create a new instance based on the previous record. The with operator will create a new instance of the object and do a shallow copy of all the members of the original object. This ensures we are not modifying data from the UI in the wrong thread. See the example:

Given the Message record:

public partial record Message(string Content, Status status, Source source);

In our Model:

...

message = message with
{
    Content = response.Message,
    Status = response.IsError ? Status.Error : Status.Value
};

//Then you can update your message list displayed in the UI, thread-safe
await Messages.UpdateAsync(message);
...

App Examples

Check out our SimpleCalc workshop and ChatGPT sample to see how we put these tips into action in real apps, using MVUX and immutable records.