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.
| Code | Severity | Summary |
|---|---|---|
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.
[BlexStore]
public class CounterStore // BLEX001
{
[BlexState] private int _count;
}[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.
[BlexAction]
private void Increment() { } // BLEX002 -- no "On" prefix
[BlexAction(Name = "Apply preset")]
private void ApplyPreset() { } // BLEX002 -- label only, still no "On"[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.
[BlexComputed]
private int DoubleCount() => Count * 2; // BLEX003[BlexComputed]
private int ComputeDoubleCount() => Count * 2; // -> DoubleCount
[BlexComputed]
private int GetDoubleCount() => Count * 2; // -> DoubleCountBLEX004 - 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.
[BlexComputed]
private int ComputeTotalFor(int rate) => Count * rate; // BLEX004[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.
[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
}[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.
public class Outer
{
[BlexStore]
public partial class Inner { } // BLEX006 -- nested
}
[BlexStore]
public partial class Cache<T> { } // BLEX006 -- generic[BlexStore]
public partial class Inner { } // top level
[BlexStore]
public partial class StringCache { } // closed over a concrete typeBLEX007 - 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.
[BlexEffect]
private async Task<User> OnLoadUser(int id) => await _api.GetAsync(id); // BLEX007
[BlexEffect]
private void OnLoadUser(int id) { } // BLEX007[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.
[BlexState] private static int _count; // BLEX008
[BlexState] private readonly int _max = 10; // BLEX008
[BlexAction] private static void OnReset() { } // BLEX008[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.
[BlexEffect(Concurrency = BlexEffectConcurrency.Latest)]
private async Task OnSearch(string query) // BLEX009 (warning)
=> Results = await _api.SearchAsync(query);[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.
[BlexAction]
private async void OnSave() => await _api.SaveAsync(); // BLEX010[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.
[BlexAction]
private int OnIncrement() => ++Count; // BLEX011 (warning) -- the caller never sees it[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.
[BlexState] private int Count; // BLEX012 -- the property would also be "Count"[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.
[BlexAction]
private void OnTryParse(string text, out int value) // BLEX013
{
int.TryParse(text, out value);
}[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.
[BlexAction]
private void OnSet<T>(T value) { } // BLEX014[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.
[BlexStore]
public partial record CounterStore // BLEX015
{
[BlexState] private int _count;
}[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.
[BlexStore]
public partial class CounterStore : MyBaseViewModel // BLEX016
{
[BlexState] private int _count;
}// 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.
[BlexComputed]
private void ComputeSummary() { } // BLEX017[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.
// 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
}[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.
[BlexEffect(DebounceMilliseconds = -1)] // BLEX019
private async Task OnSearch(string query) { }[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.
<PropertyGroup>
<NoWarn>$(NoWarn);BLEX011</NoWarn>
</PropertyGroup>#pragma warning disable BLEX011
[BlexAction] private int OnIncrement() => ++Count;
#pragma warning restore BLEX011Nothing 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.