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.
[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:
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.
Read the values repeatedly - the counters stay put. Change the state and they tick over exactly once.
- Blex T-shirt 1
- Sticker pack 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
ComputeorGetand 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.)