Blex for Blazor

Getting started

Installation

Three packages, one DI call per store, and one component wrapped around your router.

Packages

Reference Bit.Blex for the runtime, Bit.Blex.Generators for the source generator that turns your [BlexStore] classes into reactive stores, and Bit.Blex.Blazor for the Blazor integration. The generator package ships as an analyzer only (under analyzers/dotnet/cs), so it adds nothing to your app's output. Every runtime package targets net8.0, net9.0 and net10.0.

Add the packages
dotnet add package Bit.Blex
dotnet add package Bit.Blex.Generators
dotnet add package Bit.Blex.Blazor

# optional, for unit tests
dotnet add package Bit.Blex.Testing

Or directly in the project file:

XML
MyApp.csproj
<ItemGroup>
  <PackageReference Include="Bit.Blex" Version="10.7.0" />
  <PackageReference Include="Bit.Blex.Generators" Version="10.7.0" PrivateAssets="all" />
  <PackageReference Include="Bit.Blex.Blazor" Version="10.7.0" />
</ItemGroup>

Working inside this repository

The sample and docs projects live next to the sources, so they reference the generator as an analyzer project reference instead of consuming the published package: <ProjectReference Include="..\..\Bit.Blex.Generators\Bit.Blex.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />.

Register the services

AddBitBlexServices registers the manager and the middleware pipeline; AddBitBlexStore<T> registers each store as a scoped service exposed both as its concrete type and as IBlexStore. A store is attached to the manager on first resolution, so its actions are observed even in hosts without a provider component - console apps, workers and tests included.

C#
Program.cs
using Bit.Blex;
using Bit.Blex.Blazor;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");

builder.Services.AddBitBlexServices(options =>
{
    options.DevToolsName = "My App";
    options.UseMiddleware(ctx => Console.WriteLine($"[blex] {ctx.QualifiedName}"));
});

builder.Services.AddBitBlexStore<CounterStore>();
builder.Services.AddBitBlexStore<TodoStore>();

// Optional extras
builder.Services.AddBitBlexLocalStoragePersistence();
builder.Services.AddBitBlexHistory();

await builder.Build().RunAsync();

Registration APIs

Call Description
AddBitBlexServices(Action&lt;BlexOptions&gt;?) Registers BlexManager (scoped) and the middleware pipeline. The options object configures the DevTools name, sanitizers, middleware and the error sink.
AddBitBlexStore&lt;TStore&gt;() Registers a store as scoped, resolvable both as TStore and as IBlexStore. Constructor dependencies are injected, so a store can take an HttpClient like any other service.
AddBitBlexPersistence(Action&lt;BlexPersistenceOptions&gt;?) Enables durable persistence for stores marked Persist = true. Requires an IBlexStorage registration.
AddBitBlexLocalStoragePersistence(…) Blazor: registers BlexBrowserStorage over window.localStorage and calls AddBitBlexPersistence.
AddBitBlexSessionStoragePersistence(…) Blazor: the same, over window.sessionStorage - cleared when the tab closes.
AddBitBlexHistory(int maxEntries = 100) Registers BlexHistory for in-app undo/redo. <BlexProvider> starts recording once rehydration completes.

Add the provider

<BlexProvider> goes once, near the root of the app, wrapping the router. On initialization it registers every IBlexStore with the manager, restores prerendered and persisted state, and starts undo/redo recording; on first render it connects the Redux DevTools bridge.

Razor
App.razor
<BlexProvider EnableDevTools="@builder.HostEnvironment.IsDevelopment()">
    <Router AppAssembly="@typeof(App).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <p>Not found.</p>
        </NotFound>
    </Router>
</BlexProvider>

<BlexProvider> parameters

Parameter Description
EnableDevTools bool, default true. Connects the Redux DevTools bridge on first render. Set to false in production.
PersistComponentState bool, default true. Hands prerendered store state to the interactive render through Blazor's PersistentComponentState, avoiding the prerender "double render" flicker. No effect when the framework service is unavailable.
ChildContent The application content rendered inside the provider.

Turn DevTools off in production

While enabled, the entire application state tree is serialized and handed to the Redux DevTools browser extension on every action. Bind EnableDevTools to your host environment so production builds never expose application state to an installed extension. See DevTools & time travel for redaction options.

Without Blazor

The core package has no JS or Blazor dependency. In a console app, worker or test, resolve the stores from DI (which registers them with the manager) and drive them directly. For persistence, implement IBlexStorage over a file or database and call AddBitBlexPersistence().

Headless host
var services = new ServiceCollection();
services.AddBitBlexServices();
services.AddBitBlexStore<CounterStore>();

var provider = services.BuildServiceProvider();

// Resolving the store registers it with the manager.
var store = provider.GetRequiredService<CounterStore>();
var manager = provider.GetRequiredService<BlexManager>();

store.Increment();
Console.WriteLine(manager.CaptureGlobalState().ToJsonString());
// {"counter":{"Count":1,"Step":1}}

Trimming and AOT

Snapshots - SerializeState, persistence, the DevTools payload - go through System.Text.Json. By default that is the reflection-based serializer, whose metadata a trimmed publish (the Blazor WebAssembly release default) is free to strip for types it cannot see being used. Point the shared options at a source-generated context and every generated snapshot uses it instead.

C#
Program.cs
[JsonSerializable(typeof(CartLine))]
[JsonSerializable(typeof(BlexEntityState<Todo, int>))]
internal partial class AppJsonContext : JsonSerializerContext;

// Before any store is resolved or snapshotted -- the options instance becomes
// read-only on first use, so this belongs in startup.
BlexJson.Configure(o => o.TypeInfoResolver = AppJsonContext.Default);

Register the state types, not the stores

A store class is never serialized - only the value of each [BlexState] field, one property at a time. So the context needs CartLine and BlexEntityState<Todo, int>, not CartStore. A store whose state is all primitives and strings needs nothing at all.

Requirements

  • .NET 10 for Bit.Blex, Bit.Blex.Blazor and Bit.Blex.Testing.
  • netstandard2.0 for the generator, as Roslyn analyzers require.
  • Nullable reference types are supported and expected; generated code is annotated.
An unhandled error has occurred. Reload ๐Ÿ—™