Blex for Blazor

Quality

Compile-time diagnostics

The generator validates every store and fails the build with a precise message rather than emitting code that does not compile.

All nineteen diagnostics live in the Bit.Blex.Generators category. Sixteen are errors - they describe shapes the generator genuinely cannot emit - and three are warnings about code that compiles but almost certainly does not do what you meant.

CodeSeveritySummary
BLEX001 Error Store class must be partial
BLEX002 Error Cannot derive action name
BLEX003 Error Cannot derive computed name
BLEX004 Error Computed method must be parameterless
BLEX005 Error Generated member name collides
BLEX006 Error Unsupported store class shape
BLEX007 Error Effect method must return Task or ValueTask
BLEX008 Error Blex member must be a writable instance member
BLEX009 Warning Latest effect cannot cancel without a CancellationToken
BLEX010 Error Action must not be 'async void'
BLEX011 Warning Action return value is discarded
BLEX012 Error State field name equals generated property name
BLEX013 Error Unsupported parameter modifier
BLEX014 Error Generic action/effect methods are not supported
BLEX015 Error Store must be a plain class
BLEX016 Error Store cannot have another base class
BLEX017 Error Computed method must return a value
BLEX018 Warning Blex member attribute outside a store
BLEX019 Error Effect debounce cannot be negative

BLEX001 - Store class must be partial

The generator adds members through a second partial declaration, so the class has to be partial.

Rejected
[BlexStore]
public class CounterStore   // BLEX001
{
    [BlexState] private int _count;
}
Fix
[BlexStore]
public partial class CounterStore
{
    [BlexState] private int _count;
}

BLEX002 - Cannot derive action name

An action or effect method must either start with On followed by an uppercase letter, or supply a usable Name. Note that a Name containing spaces is treated as a display label only, so the method still needs the On prefix.

Rejected
[BlexAction]
private void Increment() { }              // BLEX002 -- no "On" prefix

[BlexAction(Name = "Apply preset")]
private void ApplyPreset() { }            // BLEX002 -- label only, still no "On"
Fix
[BlexAction]
private void OnIncrement() { }            // -> Increment()

[BlexAction(Name = "Bump")]
private void Increment() { }              // -> Bump()   (valid identifier)

[BlexAction(Name = "Apply preset")]
private void OnApplyPreset() { }          // -> ApplyPreset(), labelled "Apply preset"

BLEX003 - Cannot derive computed name

A computed method's name must start with Compute or Get and have something after it.

Rejected
[BlexComputed]
private int DoubleCount() => Count * 2;   // BLEX003
Fix
[BlexComputed]
private int ComputeDoubleCount() => Count * 2;   // -> DoubleCount

[BlexComputed]
private int GetDoubleCount() => Count * 2;       // -> DoubleCount

BLEX004 - Computed method must be parameterless

A computed value backs a property, so there is nowhere to pass arguments. Use an ordinary method for a parameterised projection.

Rejected
[BlexComputed]
private int ComputeTotalFor(int rate) => Count * rate;   // BLEX004
Fix
[BlexComputed]
private int ComputeTotal() => Count * Rate;   // rate lives in [BlexState]

// ... or just a plain method, with no [BlexComputed] and no memoization:
private int TotalFor(int rate) => Count * rate;

BLEX005 - Generated member name collides

Two generated members would share a name, or a generated member would collide with something you already declared or with a BlexStoreBase member. The check covers the memoization backing fields (__XxxValid, __XxxValue) and the effect fields (__XxxPending, __XxxError, __XxxCts, …) too.

Rejected
[BlexStore]
public partial class Store
{
    [BlexState] private int _count;

    [BlexComputed] private int ComputeCount() => 0;   // BLEX005 -- both want "Count"

    [BlexAction] private void OnBatch() { }           // BLEX005 -- BlexStoreBase.Batch
}
Fix
[BlexStore]
public partial class Store
{
    [BlexState] private int _count;

    [BlexComputed] private int ComputeDoubled() => Count * 2;

    [BlexAction] private void OnApplyAll() { }
}

BLEX006 - Unsupported store class shape

The generator writes a flat, top-level partial declaration, so nested and generic stores would produce broken output.

Rejected
public class Outer
{
    [BlexStore]
    public partial class Inner { }        // BLEX006 -- nested
}

[BlexStore]
public partial class Cache<T> { }         // BLEX006 -- generic
Fix
[BlexStore]
public partial class Inner { }            // top level

[BlexStore]
public partial class StringCache { }      // closed over a concrete type

BLEX007 - Effect method must return Task or ValueTask

An effect owns the loading and error lifecycle, so it cannot hand back a result. Non-generic Task or ValueTask only.

Rejected
[BlexEffect]
private async Task<User> OnLoadUser(int id) => await _api.GetAsync(id);   // BLEX007

[BlexEffect]
private void OnLoadUser(int id) { }                                       // BLEX007
Fix
[BlexEffect]
private async Task OnLoadUser(int id)
{
    User = await _api.GetAsync(id);   // results go into [BlexState]
}

BLEX008 - Blex member must be a writable instance member

[BlexState], [BlexComputed], [BlexAction] and [BlexEffect] members are per-instance and must be assignable. static, const and readonly are rejected.

Rejected
[BlexState] private static int _count;      // BLEX008
[BlexState] private readonly int _max = 10; // BLEX008

[BlexAction] private static void OnReset() { }   // BLEX008
Fix
[BlexState] private int _count;

// Constants that are not state need no attribute:
private const int Max = 10;

[BlexAction] private void OnReset() => Count = 0;

BLEX009 - Latest effect cannot cancel without a CancellationToken

Latest marks the superseded run stale so it cannot overwrite newer state - but without a token the superseded body keeps running to completion, because nothing can observe the cancellation.

Rejected
[BlexEffect(Concurrency = BlexEffectConcurrency.Latest)]
private async Task OnSearch(string query)          // BLEX009 (warning)
    => Results = await _api.SearchAsync(query);
Fix
[BlexEffect(Concurrency = BlexEffectConcurrency.Latest)]
private async Task OnSearch(string query, CancellationToken ct)
    => Results = await _api.SearchAsync(query, ct);

BLEX010 - Action must not be 'async void'

An async void method cannot be awaited, so the dispatch pipeline cannot know when it finished or record it correctly.

Rejected
[BlexAction]
private async void OnSave() => await _api.SaveAsync();   // BLEX010
Fix
[BlexAction]
private async Task OnSave() => await _api.SaveAsync();

// ... or, if it has a loading/error lifecycle:
[BlexEffect]
private async Task OnSave() => await _api.SaveAsync();

BLEX011 - Action return value is discarded

The generated wrapper returns void (or Task), so any value the implementation returns is silently dropped.

Rejected
[BlexAction]
private int OnIncrement() => ++Count;   // BLEX011 (warning) -- the caller never sees it
Fix
[BlexAction]
private void OnIncrement() => Count++;

// Callers read the result from state:
store.Increment();
var current = store.Count;

BLEX012 - State field name equals generated property name

The property is derived by trimming a leading underscore and capitalizing. A field already in that form would collide with its own property.

Rejected
[BlexState] private int Count;    // BLEX012 -- the property would also be "Count"
Fix
[BlexState] private int _count;   // -> Count
[BlexState] private int count;    // -> Count  (also fine)

BLEX013 - Unsupported parameter modifier

The wrapper captures arguments by value so they can travel as the action payload, which is impossible for a by-ref parameter.

Rejected
[BlexAction]
private void OnTryParse(string text, out int value)   // BLEX013
{
    int.TryParse(text, out value);
}
Fix
[BlexAction]
private void OnParse(string text)
    => Value = int.TryParse(text, out var value) ? value : 0;

BLEX014 - Generic action/effect methods are not supported

The generated wrapper cannot carry type parameters.

Rejected
[BlexAction]
private void OnSet<T>(T value) { }        // BLEX014
Fix
[BlexAction]
private void OnSetName(string value) => Name = value;

[BlexAction]
private void OnSetCount(int value) => Count = value;

BLEX015 - Store must be a plain class

Records get value-equality members and a synthesized clone, which fight the mutable reactive-container model a store needs.

Rejected
[BlexStore]
public partial record CounterStore        // BLEX015
{
    [BlexState] private int _count;
}
Fix
[BlexStore]
public partial class CounterStore
{
    [BlexState] private int _count;
}

// Records are ideal for the values *inside* state:
public sealed record Todo(int Id, string Title, bool Done);

BLEX016 - Store cannot have another base class

The generated partial adds : Bit.Blex.BlexStoreBase. A different base class would make the type inherit from two classes.

Rejected
[BlexStore]
public partial class CounterStore : MyBaseViewModel   // BLEX016
{
    [BlexState] private int _count;
}
Fix
// Compose instead of inheriting -- or implement an interface, which is fine.
[BlexStore]
public partial class CounterStore : ICounter
{
    private readonly MyHelper _helper = new();
    [BlexState] private int _count;
}

// Explicitly deriving from BlexStoreBase is also accepted:
[BlexStore]
public partial class CounterStore : BlexStoreBase { }

BLEX017 - Computed method must return a value

A computed backs a memoized property, so there has to be something to memoize; a void method would emit public void Xxx { get; }, which does not compile.

Rejected
[BlexComputed]
private void ComputeSummary() { }         // BLEX017
Fix
[BlexComputed]
private string ComputeSummary() => $"{Count} items";

// Something that only performs work (no value) is an action, not a computed:
[BlexAction]
private void OnRefreshSummary() { }

BLEX018 - Blex member attribute outside a store

A [BlexState], [BlexComputed], [BlexAction] or [BlexEffect] member only means something inside a [BlexStore] class. Outside one it compiles cleanly and generates nothing at all - which reads exactly like a broken generator, so this warning says so instead.

Rejected
// Forgot the [BlexStore] -- Count never appears.
public partial class CounterStore
{
    [BlexState] private int _count;    // BLEX018
}

// Same for a type nested inside a store: the nested type is not itself a store.
[BlexStore]
public partial class OuterStore
{
    [BlexState] private int _count;
    public class Inner { [BlexState] private int _x; }   // BLEX018
}
Fix
[BlexStore]
public partial class CounterStore
{
    [BlexState] private int _count;
}

BLEX019 - Effect debounce cannot be negative

DebounceMilliseconds becomes a Task.Delay in the generated wrapper, which rejects a negative delay at runtime - from inside generated code, where the stack trace explains nothing. Use 0 to disable debouncing.

Rejected
[BlexEffect(DebounceMilliseconds = -1)]      // BLEX019
private async Task OnSearch(string query) { }
Fix
[BlexEffect(DebounceMilliseconds = 300)]
private async Task OnSearch(string query) { }

// 0 (the default) means "no debouncing":
[BlexEffect]
private async Task OnSearch(string query) { }

Suppressing a warning

The three warnings are suppressible like any other. Blex's own test project suppresses BLEX011 project-wide, because it has a fixture that deliberately declares a value-returning action.

XML
MyApp.csproj
<PropertyGroup>
  <NoWarn>$(NoWarn);BLEX011</NoWarn>
</PropertyGroup>
Or locally
#pragma warning disable BLEX011
[BlexAction] private int OnIncrement() => ++Count;
#pragma warning restore BLEX011

Nothing is emitted for a rejected store

When a store fails validation, the generator reports the diagnostic and emits no partial for it. Expect a cascade of "does not contain a definition for Count" errors alongside the Blex diagnostic - fix the Blex one first and the rest disappear.

An unhandled error has occurred. Reload 🗙