This article illustrates how the Fluxor middleware’s messaging system can be applied to the default Blazor Server application to simplify its design and improve its functionality.
Introduction
A common reason why Blazor Server apps tend to use Fluxor, is to persist the state of Blazor components. So, for example, the default application’s Counter page does not reset its click count to zero every time it comes back into scope. But there is more to Fluxor than that, it’s an integrated package of Message Oriented Middleware (MOM) that can progress an entire application along clearly defined pathways.
Fluxor Patterns and Terminology
The patterns and terminology used in the following example are the ones illustrated in the Fluxor documentation. Pathways through an application are defined on a ‘use-case’ basis. These are sections of functionality that have an event that has an effect that changes the state of the application in some way. The main components of the pathway, the event, the event handler and the state management handler have to be provided but they are very loosely coupled together so that there is no need for any user defined integration software other than the provision of simple message entities. The use-case pattern for building an application differs from the more traditional tiered architecture pattern. Tiered Architecture consists of functional layers that are usually implemented as services. It's a multi-layered cake approach. The use-case pattern takes the multi-layered cake and implements it as a series of slices cut vertically through the layers. So a service may not need to be a dedicated entity if its functionality can be implemented slice by slice within use-cases.
Use-Case Examples
Two use-cases can be identified within the default Blazor Server application. A Counter use-case, where a button is clicked, then a click count is calculated, stored and displayed and a Forecast use- case where the initialization of a page results in a weather forecast being uploaded, displayed and stored. The following example of the Counter use-case shows how the Fluxor pattern of ‘event, effect, state change’ can be applied to a simple pathway.
A Counter-Use Case Example
In this example, the event is a button click, the effect is a recalculated click count and the state change is that the application gets a new count total. A good way to start building the use-case is to define the messages that make up the message trail along the pathway.
Actions
In the Fluxor documentation, messages are referred to as actions. They are simple record
types with descriptive names. Two actions are required:
public record EventCounterClicked();
public record SetCounterTotal(int TotalCount);
The action's name is usually preceded by the message type. The standard message types are:
- Events
- Commands
- Documents
Document messages hold data. My preference is to use the word Set
to describe Document actions that are used to change the state. This nomenclature can be very useful when debugging messages that have been sent or received out of sequence.
Defining the State
The state is the read-only immutable data store that’s used to hold the click count total. The state is a record
type, the definition of a state is slightly more complicated than that of an action as it needs to have a default constructor.
[FeatureState]
public record CounterState(int TotalCount)
{
public CounterState() : this(0)
{
}
}
Defining the Counter Page
The aim here is to implement a component that does one thing well. In the case of a page, that one thing is managing the UI. The component should be loosely coupled and reuseable. It does not need to know anything about any other component or have any predefined functionality such as counting the clicks and storing the click count. This is easily achieved by using the component’s OnClicked
handler to dispatch an EventCounterClicked
message to Fluxor’s message router.
@page "/counter"
@using BlazorFluxor.Store
@using BlazorFluxor.Store.CounterUseCase
@using Fluxor.Blazor.Web.Components
@using Fluxor;
@inherits FluxorComponent
<PageTitle >Counter </PageTitle >
<h1 >Counter </h1 >
<p role="status" >Current count: @CounterState!.Value.TotalCount </p >
<button class="btn btn-primary" @onclick="OnClicked" >Click me </button >
@code {
[Inject]
protected IState <CounterState >? CounterState { get; set; }
[Inject]
public IDispatcher? Dispatcher { get; set; }
private void OnClicked()
{
Dispatcher!.Dispatch(new EventCounterClicked());
}
}
The injected IState<CounterState>
instance has a Value
property that is used to reference the State
, it also has a StateChanged
event. The FluxorComponent
, that the page inherits, subscribes to the StateChanged
event and this results in the page re-rendering every time the state’s TotalCount
is updated by the middleware. Any other FluxorComponen
t that shares the same CounterState TotalCount
and is in scope will also rerender. So, for example, a shopping cart component will have its item count updated when the Count page button is clicked.
The Effects Class
Actions that have effects other than updating the state are usually handled in an Effects
class. The Effects
class name is plural but it is only a collection in the sense that it can contain multiple message handlers. The name of the action handler method can be anything but all handlers must have the same signature and be decorated with the EffectMethod
attribute. The following handler handles the EventCounterClicked
action and determines the effect that the receipt of the action has on the state of the application.
[EffectMethod]
public Task HandleEventCounterClicked
(EventCounterClicked action,IDispatcher dispatcher)
{
int totalCount = _counterState.Value.TotalCount + 5;
dispatcher.Dispatch(new SetCount (totalCount));
return Task.CompletedTask;
}
The SetCount
action has its TotalCount
property set to the updated count value and is then dispatched to the message router. The SetCount
action is handled within a Reducers
class.
The Reducers Class
The Reducers
class is the state management class. It has handlers that reduce two or more records into a single new record instance. It is the only path through which the State
can be changed. The format of this class is similar to that of the Effects
class. All handlers must have the same signature and be decorated with the ReducerMethod
attribute.
[ReducerMethod]
public static CounterState ReduceSetCounterTotal
(CounterState state,SetCounterTotal action)
{
return state with { TotalCount = action.TotalCount };
}
}
Record
types have an optimized way of updating themselves by the use of the keyword with
. What the reduce
method is doing is returning a new state instance that is the same as the old state but with the TotalCount
property updated to that of the action’s TotalCount
property
Configuration
Configuration is not as difficult as it may seem as Fluxor relates the messages to their handlers and supplies all the parameters required by the Reducer
methods and Effects
methods as well as the IDispatcher
and IState
instances that are injected into components. You do not need to populate the container with these instances, the Fluxor Service takes care of that. The service needs to be added to the builder section of the Program
class.
builder.Services.AddFluxor(options = >
{
options.ScanAssemblies(typeof(Program).Assembly);
#if DEBUG
options.UseReduxDevTools();
#endif
});
The Redux Dev Tools option is included in the code above. It is a useful browser extension that plots the message trail and can show the state’s values at each stage along the trail. The last bit of configuration that Fluxor requires is to add the tag <Fluxor.Blazor.Web.StoreInitializer/>
as the first line in App.razor
. The recommended folder structure is shown below, Store is Fluxor's root directory.
A Forecast-Use Case
In the default application, a forecast use case is handled almost entirely within the FetchData
page. That page manages both the UI and the injected WeatherForecast
service. There is no state management so new daily forecasts are loaded every time the page comes into scope. Database errors are handled by the default error handler and are not database specific. The code below implements a single page Fluxor based forecast use case that maintains the state of the page so it does not update every time it comes into scope. The UI is controlled by binding types in the page to properties in an immutable State
record and by populating the page with smart components that render only when the State
requires them. The WeatherForecast
service is implemented entirely within an Effects
class so that the page has the single responsibility of rendering the UI.
The Forecast State
The immutable State
record is defined like this:
using BlazorFluxor.Data;
using Fluxor;
using System.Collections.Immutable;
namespace BlazorFluxor.Store.ForecastUseCase
{
[FeatureState]
public record ForecastState(
ImmutableList<WeatherForecast> Forecasts,
string? Message,
bool IsError,
bool IsLoading)
{
public ForecastState() : this(ImmutableList.Create<WeatherForecast>(),
null, false, true)
{
}
}
}
The Forecasts
list holds the daily forecasts and the Message string
is for storing error messages. The two bool
s are used by the smart components to determine if they should render. The truth table below shows the possible settings of the State
’s bool
s and the component that renders for each of the four possible settings.
IsLoading | IsError | Show |
False | False | Data Table |
True | False | Spinner |
False | True | Error Dialog |
True | True | Not Defined |
The Forecast Effects Class
The WeatherForecast
service is implemented entirely within the ForecastUseCase.Effects
class so there is no need to inject the WeatherForecast
service. An asynchronous stream is used to retrieve the daily forecasts so that each daily forecast can be displayed as soon as it becomes available from the stream. This is a better option than using a method that returns a Task<IEnumerable<Weatherforecast>>
as the Task
would have to complete before any data could be displayed and the page rendering would be delayed.
[EffectMethod]
public async Task HandleEventFetchDataInitialized
(EventMsgPageInitialized action, IDispatcher dispatcher)
{
try
{
await foreach (var forecast in ForecastStreamAsync
(DateOnly.FromDateTime(DateTime.Now), 7))
{
dispatcher.Dispatch(new SetForecast(forecast, false, false));
}
}
catch (TimeoutException ex)
{
dispatcher.Dispatch(new SetDbError(ex.Message, true, false));
}
}
In order to demonstrate error handling, the GetForecastAsyncStream
method will time out the first time that it is called and the error dialog will be shown. Subsequent calls will not time out.
public async IAsyncEnumerable<WeatherForecast>
ForecastStreamAsync(DateOnly startDate, int count)
{
int timeout = _cts == null ? 1500 : 2000;
using var cts = _cts = new(timeout);
try
{
await Task.Delay(1750, _cts.Token);
}
catch (OperationCanceledException) when (_cts.Token.IsCancellationRequested)
{
throw new TimeoutException("The operation timed out.Please try again");
}
for (int i = 0; i < count; i++)
{
int temperatureIndex = Random.Shared.Next(0, Temperatures.Length);
int summaryIndex = temperatureIndex / 4;
await Task.Delay(125);
yield return new WeatherForecast
{
Date = startDate.AddDays(i),
TemperatureC = Temperatures[temperatureIndex],
Summary = Summaries[summaryIndex]
};
}
}
The method uses integer division to relate a temperature range to an appropriate summary value:
private static readonly string[] Summaries = new[]
{
"Freezing", "Cold", "Mild", "Hot"
};
private static readonly int[] Temperatures = new[]
{
0,-2,-4,-6,
2,6,8,10,
12,14,16,18,
23,24,26,28
};
The Forecast Reducers Class
A reducer updates the Forecasts
list by calling the list’s Add
method. The method is designed so that it creates a new updated instance of the list without the usual overhead associated with adding values and creating a new list.
public static ForecastState ReduceSetForecast(ForecastState state, SetForecast action)
{
return state with
{
Forecasts = state.Forecasts.Add(action.Forecast),
IsError = action.IsError,
IsLoading = action.IsLoading
};
}
The WeatherForecast Table
The WeatherForecastTable
is an example of a smart component. It simply uses a template table with an added IsShow bool
, if this is set to true
, the component will render.
@if (IsShow)
{
<TableTemplate Items="Forecasts" Context="forecast">
<TableHeader>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</TableHeader>
<RowTemplate>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</RowTemplate>
</TableTemplate>
}
@code {
#nullable disable
[Parameter]
public IEnumerable<WeatherForecast> Forecasts { get; set; }
[Parameter]
public bool IsShow { get; set; }
}
The FetchData Page
@inherits FluxorComponent
@inject NavigationManager NavManager
<PageTitle>Weather Forecasts</PageTitle>
<h1>Weather Forecasts</h1>
<p>This component simulates fetching data from an async stream.
Each forecast is listed as soon as it is available.</p>
<Spinner IsVisible=@IsShowSpinner />
<WeatherForecastTable IsShow="@IsShowTable" Forecasts="@Forecasts" />
<TemplatedDialog IsShow="@IsShowError">
<OKDialog Heading="Error"
BodyText="Whoops, an error has occurred."
OnOK="NavigateToIndex">@ForecastState!.Value.Message</OKDialog>
</TemplatedDialog>
The Spinner
is available as a NuGet package and the templated components are based on the examples in the .NET Foundation’s Blazor-Workshop. The code section below is mainly concerned with simplifying the logic for displaying each of the components.
@code {
[Inject]
protected IState<ForecastState>? ForecastState { get; set; }
[Inject]
public IDispatcher? Dispatcher { get; set; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (ForecastState!.Value.IsLoading is true)
{
Dispatcher!.Dispatch(new EventMsgPageInitialized());
}
}
protected IEnumerable<WeatherForecast> Forecasts => ForecastState!.Value.Forecasts!;
protected bool IsShowError => (ForecastState!.Value.IsLoading is false &&
ForecastState!.Value.IsError is true);
protected bool IsShowTable => (ForecastState!.Value.IsLoading is false &&
ForecastState!.Value.IsError is false);
protected bool IsShowSpinner => (ForecastState!.Value.IsLoading is true &&
ForecastState!.Value.IsError is false);
private void NavigateToIndex()
{
Dispatcher!.Dispatch(new SetStateToNew());
NavManager.NavigateTo("/");
}
}
Conclusion
The examples illustrate the benefits of using Fluxor. It produces clear pathways that scale well and are easy to follow and debug. In the example, only one use-case is considered but in an enterprise application, there will be many. Everyone of them will require actions, effect handlers, reducers and a state so the code base is going to be large. But that's ok as we all believe in the maxim that verbose code can be smart code. Don't we?
Acknowledgement
I would like to acknowledge Peter Morris, the author of Fluxor. He is an excellent developer and his GitHub page is well worth following.
History
- 4th July, 2023: Initial version
- 8th August, 2023: Added a Fetch Data example