Abies (/ˈa.bi.eːs/)

May 1, 2026 · View on GitHub

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.

.NET License NuGet CD E2E Tests Benchmarks

Why Abies?

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

Render Modes

Abies supports four render modes — the same spectrum as Blazor, but built on pure MVU:

ModeInitial HTMLInteractivityUse Case
StaticServerNoneSEO pages, content, zero JS
InteractiveServerServerServer (WebSocket)Instant interaction, no WASM download
InteractiveWasmServerClient (WASM)Offline-capable, no persistent connection
InteractiveAutoServerServer → ClientBest 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());

Quick Start

# Install the Abies templates
dotnet new install Picea.Abies.Templates

# Create a browser (WASM) app
dotnet new abies-browser -n MyApp

cd MyApp
dotnet run

Counter Example

using Picea.Abies;
using static Picea.Abies.Html.Elements;
using static Picea.Abies.Html.Attributes;
using static Picea.Abies.Html.Events;

await Picea.Abies.Browser.Runtime.Run<Counter, Model, Arguments>(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;
}

Architecture

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:

PlatformApply Implementation
Browser (Picea.Abies.Browser)JS interop → mutate real DOM
Server (Picea.Abies.Server)Binary batch → WebSocket → client-side JS
TestsCapture patches for assertions

Subscriptions

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))
    ]);

Performance: Abies Browser vs Blazor WASM

Measured with js-framework-benchmark on the same machine, same session.

Duration Benchmarks (lower is better)

Latest same-session validation (2026-04-02, AC power, local main baseline):

  • Full 9-benchmark duration suite rerun against Blazor WASM (same machine/session)
  • Geometric mean (total medians): 0.62x for Abies vs 1.62x for Blazor
  • Duration, startup/size, and memory tables below reflect the fresh 2026-04-03 run

Details: Render StringBuilder Pool Cap Validation (2026-04-02)

BenchmarkAbies 2.0Blazor 10.0Delta
Create 1,000 rows119.5 ms89.5 ms+34%
Replace 1,000 rows124.5 ms106.7 ms+17%
Update every 10th row ×1680.4 ms93.6 ms−14%
Select row13.3 ms80.3 ms−83%
Swap rows30.9 ms93.5 ms−67%
Remove row19.9 ms93.9 ms−79%
Create 10,000 rows1183.5 ms805.3 ms+47%
Append 1,000 rows133.0 ms113.0 ms+18%
Clear 1,000 rows ×818.9 ms40.0 ms−53%
Geometric mean0.62×1.62×−38%

Startup & Size (lower is better)

MetricAbies 2.0Blazor 10.0Delta
First paint71.1 ms79.4 ms−10%
Framework bundle artifact (compressed, js-framework-benchmark 40_sizes)116 KB1,078 KB−89%
Framework bundle artifact (uncompressed, js-framework-benchmark 40_sizes)454 KB3,400 KB−87%

Note: These size rows come from js-framework-benchmark's 40_sizes artifact metric. They are not equivalent to full .NET publish output size reported in docs/benchmarks.md.

Memory (lower is better)

MetricAbies 2.0Blazor 10.0Delta
Ready memory35.1 MB41.1 MB−15%
Run memory37.2 MB52.6 MB−29%
Clear memory59.3 MB49.4 MB+20%

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.

Example Application: Conduit

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

See Picea.Abies.Conduit/README.md for the Conduit-specific layout, local run commands, and test strategy.

# 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.Host

Project Structure

Core Framework

ProjectDescription
Picea.AbiesCore MVU library — virtual DOM, diffing, rendering, subscriptions
Picea.Abies.BrowserBrowser runtime — WASM host, JS interop, real DOM patching
Picea.Abies.ServerServer runtime — SSR, websocket patch transport
Picea.Abies.Server.KestrelKestrel integration — WebSocket endpoints, static files
Picea.Abies.Templatesdotnet new project templates (abies-browser, abies-browser-empty)
Picea.Abies.AnalyzersRoslyn analyzers for compile-time HTML checks

Sample Applications

ProjectDescription
Picea.Abies.CounterMinimal counter example (shared logic)
Picea.Abies.Counter.WasmCounter — WASM hosting
Picea.Abies.Counter.ServerCounter — server-side hosting
Picea.Abies.ConduitRealWorld app — domain model
Picea.Abies.Conduit.AppRealWorld app — MVU frontend
Picea.Abies.Conduit.Wasm.HostRealWorld app — WASM hosting
Picea.Abies.Conduit.ServerRealWorld app — server hosting
Picea.Abies.Conduit.ApiRealWorld app — REST API
Picea.Abies.PresentationConference presentation app

Infrastructure

ProjectDescription
Picea.Abies.Conduit.AppHost.NET Aspire orchestration
Picea.Abies.Conduit.ServiceDefaultsShared defaults (OpenTelemetry, health checks)
Picea.Abies.BenchmarksBenchmarkDotNet micro-benchmarks
contrib/js-framework-benchmarkjs-framework-benchmark entry point
Picea.Abies.TestsUnit tests
Picea.Abies.Server.TestsServer runtime tests
Picea.Abies.Server.Kestrel.TestsKestrel integration tests
Picea.Abies.Conduit.Testing.E2EEnd-to-end Playwright tests
Picea.Abies.Counter.Testing.E2ECounter E2E tests

Observability

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/traces proxy endpoint

Configurable verbosity levels: off, user (default), debug.

<meta name="otel-verbosity" content="user">

See Tutorial 8: Tracing for a full walkthrough.

Requirements

  • .NET 10 SDK or later
  • A modern browser with WebAssembly support (for WASM mode)

Building

dotnet build
dotnet test

Documentation

See the docs folder for comprehensive documentation:

Getting Started

Concepts

Tutorials

  1. Counter App — Basic MVU
  2. Todo List — Managing collections
  3. API Integration — HTTP commands
  4. Routing — Multi-page navigation
  5. Forms — Input handling & validation
  6. Subscriptions — Timers, resize, WebSocket
  7. Real-World App — Conduit walkthrough
  8. Tracing — OpenTelemetry integration

Guides

Contributing

We welcome contributions! Abies follows trunk-based development with protected main branch.

  1. Fork and clone the repository
  2. Create a feature branch from main
  3. Make your changes with tests
  4. Ensure CI passes: dotnet build, dotnet test, dotnet format --verify-no-changes
  5. Submit a pull request following our PR template

For detailed guidelines, see CONTRIBUTING.md.

The Name

Abies is a Latin name meaning "fir tree" — a species in the genus Picea.

Pronunciation

  • 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

License

Apache 2.0 · Copyright Maurice Peters