A full-stack Model-View-Update (MVU) framework for .NET — build interactive web applications with pure functions, from server-rendered HTML to client-side WebAssembly.
Abies brings the Elm Architecture to .NET with a twist: one codebase, four render modes. Write your UI as pure functions and deploy it however you need — static HTML, interactive server, client-side WASM, or auto (server-first with WASM handoff).
- Pure functional architecture — no side effects in your domain logic
- Virtual DOM with efficient keyed diffing and binary batch patching
- Type-safe routing with C# pattern matching on URL segments
- Full-stack tracing with OpenTelemetry (browser → server)
- Built on Picea — the Mealy machine kernel that powers MVU, Event Sourcing, and Actor runtimes
Abies supports four render modes — the same spectrum as Blazor, but built on pure MVU:
| Mode | Initial HTML | Interactivity | Use Case |
|---|---|---|---|
| Static | Server | None | SEO pages, content, zero JS |
| InteractiveServer | Server | Server (WebSocket) | Instant interaction, no WASM download |
| InteractiveWasm | Server | Client (WASM) | Offline-capable, no persistent connection |
| InteractiveAuto | Server | Server → Client | Best UX: instant interaction + WASM handoff |
All four modes share the same Program<TModel, TArgument> interface. Your MVU code doesn't change — only the hosting configuration does.
// Static — one-shot HTML, zero JavaScript
var html = Page.Render<MyApp, MyModel, Unit>(RenderMode.Static);
// InteractiveServer — patches over WebSocket
var html = Page.Render<MyApp, MyModel, Unit>(new RenderMode.InteractiveServer());
// InteractiveWasm — client-side .NET runtime
var html = Page.Render<MyApp, MyModel, Unit>(new RenderMode.InteractiveWasm());
// InteractiveAuto — server-first, transitions to WASM
var html = Page.Render<MyApp, MyModel, Unit>(new RenderMode.InteractiveAuto());# Install the Abies templates
dotnet new install Picea.Abies.Templates
# Create a browser (WASM) app
dotnet new abies-browser -n MyApp
cd MyApp
dotnet runusing Picea.Abies;
using static Picea.Abies.Html.Elements;
using static Picea.Abies.Html.Attributes;
using static Picea.Abies.Html.Events;
await Runtime.Run<Counter, Arguments, Model>(new Arguments());
public record Arguments;
public record Model(int Count);
public record Increment : Message;
public record Decrement : Message;
public class Counter : Program<Model, Arguments>
{
public static (Model, Command) Initialize(Arguments argument)
=> (new Model(0), Commands.None);
public static (Model, Command) Transition(Model model, Message message)
=> message switch
{
Increment => (model with { Count = model.Count + 1 }, Commands.None),
Decrement => (model with { Count = model.Count - 1 }, Commands.None),
_ => (model, Commands.None)
};
public static Document View(Model model)
=> new("Counter",
div([], [
button([onclick(new Decrement())], [text("-")]),
text(model.Count.ToString()),
button([onclick(new Increment())], [text("+")])
]));
public static Subscription Subscriptions(Model model) => SubscriptionModule.None;
}Abies is built on the Picea kernel — a Mealy machine abstraction that provides the core (State, Event) → (State, Effect) transition function. Abies specializes this for MVU:
Message
→ Transition(model, message) → (model', command)
→ Observer: View(model') → Document → Diff → Patches → Apply
→ Interpreter: command → Result<Message[], PipelineError>
→ Dispatch each feedback message (recurse)
The Apply delegate is the seam between pure Abies core and platform-specific rendering:
| Platform | Apply Implementation |
|---|---|
Browser (Picea.Abies.Browser) |
JS interop → mutate real DOM |
Server (Picea.Abies.Server) |
Binary batch → WebSocket → client-side JS |
| Tests | Capture patches for assertions |
Subscriptions let you react to external event sources without putting side effects in Transition:
public record Tick : Message;
public record ViewportChanged(ViewportSize Size) : Message;
public record SocketEvent(WebSocketEvent Event) : Message;
public static Subscription Subscriptions(Model model) =>
SubscriptionModule.Batch([
SubscriptionModule.Every(TimeSpan.FromSeconds(1), _ => new Tick()),
SubscriptionModule.OnResize(size => new ViewportChanged(size)),
SubscriptionModule.WebSocket(
new WebSocketOptions("wss://example.com/socket"),
evt => new SocketEvent(evt))
]);Abies outperforms Blazor WASM across every duration benchmark. Measured with js-framework-benchmark on the same machine, same session.
| Benchmark | Abies 2.0 | Blazor 10.0 | Delta |
|---|---|---|---|
| Create 1,000 rows | 51.7 ms | 84.9 ms | −39% |
| Replace 1,000 rows | 60.2 ms | 99.5 ms | −39% |
| Update every 10th row ×16 | 67.1 ms | 94.5 ms | −29% |
| Select row | 13.8 ms | 82.5 ms | −83% |
| Swap rows | 33.8 ms | 94.6 ms | −64% |
| Remove row | 20.7 ms | 40.2 ms | −49% |
| Create 10,000 rows | 550.0 ms | 766.1 ms | −28% |
| Append 1,000 rows | 76.2 ms | 102.9 ms | −26% |
| Clear 1,000 rows ×8 | 18.3 ms | 36.5 ms | −50% |
| Geometric mean | 1.00× | 1.98× |
| Metric | Abies 2.0 | Blazor 10.0 | Delta |
|---|---|---|---|
| First paint | 57.7 ms | 78.0 ms | −26% |
| Bundle (compressed) | 1,139 KB | 1,377 KB | −17% |
| Bundle (uncompressed) | 3,729 KB | 4,208 KB | −11% |
| Metric | Abies 2.0 | Blazor 10.0 | Delta |
|---|---|---|---|
| Ready memory | 34.4 MB | 41.1 MB | −16% |
| Run memory | 36.2 MB | 52.7 MB | −31% |
| Clear memory | 58.6 MB | 49.4 MB | +19% |
Note: Clear memory is higher in Abies due to lazy GC in the WASM runtime. All other metrics show Abies ahead.
📊 Interactive Benchmark Charts — Historical trends on GitHub Pages
See docs/benchmarks.md for details on running benchmarks locally.
The repository includes Conduit, a full implementation of the RealWorld specification — a Medium.com clone demonstrating:
- User authentication (login/register)
- Article CRUD with Markdown rendering
- Comments and favorites
- User profiles and following
- Tag filtering and pagination
- Both WASM and server-rendered hosting modes
- REST API with PostgreSQL read store
- E2E tests with Playwright
- .NET Aspire orchestration for local development
# Run with .NET Aspire (recommended)
dotnet run --project Picea.Abies.Conduit.AppHost
# Or run individually
dotnet run --project Picea.Abies.Conduit.Api &
dotnet run --project Picea.Abies.Conduit.Wasm| Project | Description |
|---|---|
Picea.Abies |
Core MVU library — virtual DOM, diffing, rendering, subscriptions |
Picea.Abies.Browser |
Browser runtime — WASM host, JS interop, real DOM patching |
Picea.Abies.Server |
Server runtime — SSR, Session, Page, RenderMode, Transport |
Picea.Abies.Server.Kestrel |
Kestrel integration — WebSocket endpoints, static files |
Picea.Abies.Templates |
dotnet new project templates (abies-browser, abies-browser-empty) |
Picea.Abies.Analyzers |
Roslyn analyzers for compile-time HTML checks |
| Project | Description |
|---|---|
Picea.Abies.Counter |
Minimal counter example (shared logic) |
Picea.Abies.Counter.Wasm |
Counter — WASM hosting |
Picea.Abies.Counter.Server |
Counter — server-side hosting |
Picea.Abies.Conduit |
RealWorld app — domain model |
Picea.Abies.Conduit.App |
RealWorld app — MVU frontend |
Picea.Abies.Conduit.Wasm |
RealWorld app — WASM hosting |
Picea.Abies.Conduit.Server |
RealWorld app — server hosting |
Picea.Abies.Conduit.Api |
RealWorld app — REST API |
Picea.Abies.Presentation |
Conference presentation app |
| Project | Description |
|---|---|
Picea.Abies.Conduit.AppHost |
.NET Aspire orchestration |
Picea.Abies.ServiceDefaults |
Shared defaults (OpenTelemetry, health checks) |
Picea.Abies.Benchmarks |
BenchmarkDotNet micro-benchmarks |
contrib/js-framework-benchmark |
js-framework-benchmark entry point |
Picea.Abies.Tests |
Unit tests |
Picea.Abies.Server.Tests |
Server runtime tests |
Picea.Abies.Server.Kestrel.Tests |
Kestrel integration tests |
Picea.Abies.Conduit.Testing.E2E |
End-to-end Playwright tests |
Picea.Abies.Counter.Testing.E2E |
Counter E2E tests |
Abies provides full-stack OpenTelemetry tracing out of the box:
- Browser — DOM event spans, fetch request propagation (via
abies.js) - Server — Session lifecycle, page render, message dispatch spans
- Runtime — Message processing, command execution, model update spans
- OTLP export — Browser traces export to
/otlp/v1/tracesproxy endpoint
Configurable verbosity levels: off, user (default), debug.
<meta name="otel-verbosity" content="user">See Tutorial 8: Tracing for a full walkthrough.
- .NET 10 SDK or later
- A modern browser with WebAssembly support (for WASM mode)
dotnet build
dotnet testSee the docs folder for comprehensive documentation:
- MVU Architecture
- Render Modes — Static, Server, WASM, Auto
- Virtual DOM
- Commands & Effects
- Subscriptions
- Pure Functions
- Counter App — Basic MVU
- Todo List — Managing collections
- API Integration — HTTP commands
- Routing — Multi-page navigation
- Forms — Input handling & validation
- Subscriptions — Timers, resize, WebSocket
- Real-World App — Conduit walkthrough
- Tracing — OpenTelemetry integration
We welcome contributions! Abies follows trunk-based development with protected main branch.
- Fork and clone the repository
- Create a feature branch from
main - Make your changes with tests
- Ensure CI passes:
dotnet build,dotnet test,dotnet format --verify-no-changes - Submit a pull request following our PR template
For detailed guidelines, see CONTRIBUTING.md.
Abies is a Latin name meaning "fir tree" — a species in the genus Picea.
- A: as in "father" [a]
- bi: as in "machine" [bi]
- es: as in "they" but shorter [eːs]
Stress: First syllable (A-bi-es) · Phonetic: AH-bee-ehs
Apache 2.0 · Copyright Maurice Peters