Blex for Blazor

Core concepts

Computed state

Derived values that recompute only when they have to, and invalidate themselves whenever state changes.

The convention

Mark a parameterless method named ComputeXxx() or GetXxx() with [BlexComputed] and the generator emits a memoized public property Xxx. The value is calculated on first read, cached, and thrown away the moment any [BlexState] field on that store changes.

C#
[BlexStore(Name = "cart")]
public partial class CartStore
{
    [BlexState] private IReadOnlyList<CartLine> _lines = [];
    [BlexState] private decimal _discountRate;

    [BlexComputed] private decimal ComputeSubtotal() => Lines.Sum(l => l.Total);

    // Computed values may build on other computed values.
    [BlexComputed] private decimal ComputeTotal() => Math.Round(Subtotal * (1 - DiscountRate), 2);

    [BlexComputed] private int ComputeItemCount() => Lines.Sum(l => l.Quantity);
}

Which becomes, in the generated partial:

Generated
private bool __SubtotalValid;
private decimal __SubtotalValue = default!;

public decimal Subtotal
{
    get
    {
        if (!__SubtotalValid)
        {
            __SubtotalValue = ComputeSubtotal();
            __SubtotalValid = true;
        }
        return __SubtotalValue;
    }
}

// Called by BlexStoreBase after every state change.
protected override void InvalidateComputed()
{
    __SubtotalValid = false;
    __TotalValid = false;
    __ItemCountValid = false;
}

Memoization in practice

Reading a computed property ten times evaluates it once. Only a state change makes it stale. The counters below are incremented inside the compute methods, so they show exactly how often each one actually ran.

Live demo Count the evaluations

Read the values repeatedly - the counters stay put. Change the state and they tick over exactly once.

Subtotal ¤37.00
Total ¤37.00
Items 3
Subtotal evaluated 1×
Total evaluated 1×
  • Blex T-shirt ¤24.00 each 1
  • Sticker pack ¤6.50 each 2

Composing computed values

A computed method may read other computed properties. ComputeTotal() above reads Subtotal, so the first read of Total after a change evaluates both, in dependency order, with no wiring on your part.

Invalidation is store-wide, not per-dependency

Blex does not track which fields a computed method touched. Any state change on the store invalidates every computed value on it. That keeps invalidation correct and free of bookkeeping, but it means an expensive computation on a busy store recomputes more often than strictly necessary. If that matters, split the store, or cache inside the method against the specific inputs.

Rules

  • The method must be parameterless - a computed value backs a property, so there is nowhere to pass arguments (BLEX004).
  • The name must start with Compute or Get and have something after it (BLEX003).
  • It must be an instance method, not static (BLEX008).
  • The generated property name must not collide with anything else on the store (BLEX005).
  • Any return type is allowed, including collections and records.

Computed values are not serialized

Only [BlexState] fields go into SerializeState(). Computed values are derived, so they are recalculated after a restore rather than stored - which means a time-travel jump or a rehydrate can never leave a derived value out of sync with the state it came from.

Keep compute methods pure

A compute method should read state and return a value. Do not mutate state or start async work in one: it runs lazily, at unpredictable times, from inside a property getter. (The evaluation counters in the demo above break that rule deliberately, to make the memoization visible.)

An unhandled error has occurred. Reload 🗙