Skip to the content.

Part 3: State: IState<T> and Its flavors

Video covering this part (a bit outdated due to API changes):

You already know two key concepts of Fusion:

  1. Compute Services allow to write functions that compute everything just once and keep the result cached till the moment it either stopped being used, or one of its dependencies (a similar output) gets invalidated.
  2. Computed<T> – an abstraction that’s actually responsible for tracking all these dependencies.

The last missing piece of a puzzle is IState<T>, or simply speaking, a “state”. If you used Knockout.js or MobX earlier, state would correspond to their versions of computed values - i.e. in fact, state simply tracks the most recent version of some Computed<T>.

Ok, probably it’s not quite clear what I just said, but remember that:

So IState<T> is what “tracks” the most up-to-date version. There are two implementations of IState<T>:

Let’s summarize all of this in a single table:

And finally, states have a few extra properties:

The interface diagram for IState<T> and its “friends” (arrows with no label show inheritance):

Constructing States

There are two ways of doing this:

  1. Using IStateFactory. Any IServiceProvider configured to use Fusion (with .AddFusionCore()) should resolve it.
  2. Subclassing MutableState<T>, ComputedState<T>, etc. and either creating its instance manually, or registering a new type as a service via .AddState(...) method and resolving it via IServiceProvider.

Normally you need just the first option. The remaining part of this document relies on it.

Mutable State

Time to write some code! We’ll be using the same “stub” with CounterService and CreateServices here:

``` cs –editable false –region Part03_CounterService –source-file Part03.cs public class CounterService : IComputeService { private readonly ConcurrentDictionary<string, int> _counters = new ConcurrentDictionary<string, int>();

[ComputeMethod]
public virtual async Task<int> Get(string key)
{
    WriteLine($"{nameof(Get)}({key})");
    return _counters.TryGetValue(key, out var value) ? value : 0;
}

public void Increment(string key)
{
    WriteLine($"{nameof(Increment)}({key})");
    _counters.AddOrUpdate(key, k => 1, (k, v) => v + 1);
    using (Computed.Invalidate())
        _ = Get(key);
} }

public static IServiceProvider CreateServices() { var services = new ServiceCollection(); services.AddFusion().AddService(); return services.BuildServiceProvider(); }


Here is how you use `MutableState<T>`:

``` cs --region Part03_MutableState --source-file Part03.cs
var services = CreateServices();
var stateFactory = services.StateFactory();
var state = stateFactory.NewMutable<int>(1);
var computed = state.Computed;
WriteLine($"Value: {state.Value}, Computed: {state.Computed}");
state.Value = 2;
WriteLine($"Value: {state.Value}, Computed: {state.Computed}");
WriteLine($"Old computed: {computed}"); // Should be invalidated

The output:

Value: 1, Computed: StateBoundComputed`1(MutableState`1(#63646052) @2AULKFZxjG, State: Consistent)
Value: 2, Computed: StateBoundComputed`1(MutableState`1(#63646052) @2AULKFZxlK, State: Consistent)
Old computed: StateBoundComputed`1(MutableState`1(#63646052) @2AULKFZxjG, State: Invalidated)

Note that:

Let’s look at error handling example:

``` cs –region Part03_MutableStateError –source-file Part03.cs var services = CreateServices(); var stateFactory = services.StateFactory(); var state = stateFactory.NewMutable(); WriteLine($"Value: {state.Value}, Computed: {state.Computed}"); WriteLine("Setting state.Error."); state.Error = new ApplicationException("Just a test"); try { WriteLine($"Value: {state.Value}, Computed: {state.Computed}"); } catch (ApplicationException) { WriteLine($"Error: {state.Error.GetType()}, Computed: {state.Computed}"); } WriteLine($"LastNonErrorValue: {state.LastNonErrorValue}"); WriteLine($"Snapshot.LastNonErrorComputed: {state.Snapshot.LastNonErrorComputed}");


The output:

```text
Value: 0, Computed: StateBoundComputed`1(MutableState`1(#63646052) @caJUviqcf, State: Consistent)
Setting state.Error.
Error: System.ApplicationException, Computed: StateBoundComputed`1(MutableState`1(#63646052) @caJUviqej, State: Consistent)
LastNonErrorValue: 0
Snapshot.LastNonErrorComputed: StateBoundComputed`1(MutableState`1(#63646052) @caJUviqcf, State: Invalidated)

As you see, Value property throws an exception here – as per IResult<T> contract, it re-throws an exception stored in Error.

The last “valid” value is still available via LastNonErrorValue property; similarly, the last “valid” computed instance is still available via LatestNonErrorValueComputed.

Computed State

Let’s play with IComputedState<T> now:

``` cs –region Part03_LiveState –source-file Part03.cs var services = CreateServices(); var counters = services.GetRequiredService(); var stateFactory = services.StateFactory(); WriteLine("Creating state."); using var state = stateFactory.NewComputed( new ComputedState.Options() { UpdateDelayer = FixedDelayer.Get(1), // 1 second update delay EventConfigurator = state1 => { // A shortcut to attach 3 event handlers: Invalidated, Updating, Updated state1.AddEventHandler(StateEventKind.All, (s, e) => WriteLine($"{DateTime.Now}: {e}, Value: {s.Value}, Computed: {s.Computed}")); }, }, async (state, cancellationToken) => { var counter = await counters.Get("a"); return $"counters.Get(a) -> {counter}"; }); WriteLine("Before state.Update(false)."); await state.Update(); // Ensures the state gets up-to-date value WriteLine("After state.Update(false)."); counters.Increment("a"); await Task.Delay(2000); WriteLine($"Value: {state.Value}, Computed: {state.Computed}");


The output:

```text


Creating state.
10/2/2020 6:26:04 AM: Updated, Value: , Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @26, State: Consistent)
10/2/2020 6:26:04 AM: Invalidated, Value: , Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @26, State: Invalidated)
Before state.Update(false).
10/2/2020 6:26:04 AM: Updating, Value: , Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @26, State: Invalidated)
Get(a)
10/2/2020 6:26:04 AM: Updated, Value: counters.Get(a) -> 0, Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @4a, State: Consistent)
After state.Update(false).
Increment(a)
10/2/2020 6:26:04 AM: Invalidated, Value: counters.Get(a) -> 0, Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @4a, State: Invalidated)
10/2/2020 6:26:05 AM: Updating, Value: counters.Get(a) -> 0, Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @4a, State: Invalidated)
Get(a)
10/2/2020 6:26:05 AM: Updated, Value: counters.Get(a) -> 1, Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @29, State: Consistent)
Value: counters.Get(a) -> 1, Computed: StateBoundComputed`1(FuncLiveState`1(#66697461) @29, State: Consistent)

Some observations:

Next: Part 4 » | Tutorial Home