Download Sample Code

Single Source of Truth

In modern websites, some pieces of information may appear in multiple places, such as the profile avatar, the website language, theme, etc. One way to ensure data consistency and accuracy is to implement the Single Source of Truth (SSOT) pattern, which centralises all data instead of distributing it across multiple components. In this tutorial, we will go through:

  • The need for a single source of truth.
  • Implementation approaches.
  • CascadingValueSource approach.
  • Scoped Service approach.

The Need For a Single Source of Truth

In Blazor, maintaining consistent data across components is essential, particularly in features like profile management. Consider the scenario profile management as in the image:

why-ssot.webp

When the user inputs data and the change is accepted, it should update the display name on the menu bar and the profile page. Without SSOT, developers might risk:

  • Data inconsistency and inaccuracy.
  • Difficulty in updating data in multiple places.
  • An unfriendly SPA where users have to reload the page to see new data.

With data centralised with SSOT, developers can easily find the data needed to display. Furthermore, when changes are applied to the data source, all components will be automatically updated. This ensures data consistency and accuracy throughout the app and reduces the effort to maintain such data.


Implementation Approaches

There are 2 ways to implement the SSOT pattern in Blazor:

  • Using CascadingValueSource: This method leverages a cascading parameter with a source that automatically notifies and updates all dependent components when the central data changes. It’s ideal for real-time updates, such as reflecting a new display name across the app, with minimal manual intervention.
  • Using a Scoped Service: This approach involves a service registered as scoped, requiring manual updates to components via event handling or state management. It offers more control but demands additional code, making it suitable for legacy projects or complex state logic.

Best practice

For new projects, CascadingValueSource is typically the better option due to its simplicity and automatic updates. Use a scoped service when integrating with older codebases.


CascadingValueSource Approach

  1. Define the data-holding class: Create a class with a CascadingValueSource<T> property, where T is the class itself:
public class CascadingParameterSharedData
{
    public int Value { get; set; }

    public CascadingValueSource<CascadingParameterSharedData> Source { get; set; }
}
  1. Register the class in Program.cs: While registering the class, set the source value.
builder.Services.AddCascadingValue(sp =>
{
    var value = new CascadingParameterSharedData();
    value.Source = new(value, false); // false indicates no fixed value

    return value.Source;
});
  1. Use the Cascading Parameter in Components:
<div>Value: @SharedData.Value</div>
<button type="button" @onclick="ChangeValue">Change Value</button>

@code {
    [CascadingParameter]
    public CascadingParameterSharedData SharedData { get; set; }

    public async Task ChangeValue()
    {
        var random = new Random();
        SharedData.Value = random.Next(1, 100);
        await SharedData.Source.NotifyChangedAsync();
    }
}
After modifying the shared data (e.g., SharedData.Value), always invoke NotifyChangedAsync to propagate the change. Failing to do so will leave other components using the old value, breaking the SSOT principle and causing rendering discrepancies.

Scoped Service Approach

  1. Define the data-holding class: Create a class with data and a notification mechanism:
public class BlazorSchoolTransferService
{
    public string Message { get; set; } = "";
    public event EventHandler MessageChanged = (sender, args) => { };

    public void NotifyChanged()
    {
        MessageChanged.Invoke(this, EventArgs.Empty);
    }
}
  1. Register the class in Program.cs:
builder.Services.AddScoped<BlazorSchoolTransferService>();
  1. Inject and use the service in components:
@inject BlazorSchoolTransferService TransferService
@implements IDisposable

<div class="bg-primary p-5">
    <h3>Component1</h3>
    <div>Value: @TransferService.Message</div>
    <button type="button" @onclick="ChangeValue">Change Value</button>
    <Component2 />
</div>

@code {
    protected override void OnInitialized()
    {
        TransferService.MessageChanged += OnTransferServiceChanged;
    }

    public void OnTransferServiceChanged(object? sender, EventArgs e)
    {
        StateHasChanged();
    }

    public void ChangeValue()
    {
        var random = new Random();
        TransferService.Message = "Message updated from Component1";
        TransferService.NotifyChanged();
    }

    public void Dispose()
    {
        TransferService.MessageChanged -= OnTransferServiceChanged;
    }
}
  • Event Subscription: Every component accessing data must subscribe to change event (e.g., in OnInitialized) and call StateHasChanged to update the UI when notified. The number of events depends on the requirement. It could be one event for multiple properties or one event per property.
  • Notification: After modifying data, always call notify method to propagate the change to all subscribers.
  • Unsubscription: Unsubscribe from the event in Dispose to prevent memory leaks.
BLAZOR SCHOOL
Designed and built with care by our dedicated team, with contributions from a supportive community. We strive to provide the best learning experience for our users.
Docs licensed CC-BY-SA-4.0
Copyright © 2021-2025 Blazor School
An unhandled error has occurred. Reload 🗙