Picea 1.0.34-rc-0002

This is a prerelease version of Picea.
dotnet add package Picea --version 1.0.34-rc-0002
                    
NuGet\Install-Package Picea -Version 1.0.34-rc-0002
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Picea" Version="1.0.34-rc-0002" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Picea" Version="1.0.34-rc-0002" />
                    
Directory.Packages.props
<PackageReference Include="Picea" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Picea --version 1.0.34-rc-0002
                    
#r "nuget: Picea, 1.0.34-rc-0002"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Picea@1.0.34-rc-0002
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Picea&version=1.0.34-rc-0002&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Picea&version=1.0.34-rc-0002&prerelease
                    
Install as a Cake Tool

Picea

Write your domain logic once. Run it everywhere.

A minimal, production-hardened Mealy machine kernel for building state machines, MVU runtimes, event-sourced aggregates, and actor systems in .NET — based on the observation that all three are instances of the same mathematical structure: a Mealy machine (finite-state transducer with effects).

transition : (State × Event) → (State × Effect)

Define your pure domain logic once as a transition function. Then plug it into any runtime — a browser UI loop, an event-sourced aggregate, or a mailbox actor — without changing a single line.

Installation

dotnet add package Picea

The Kernel

public interface Automaton<TState, TEvent, TEffect, TParameters>
{
    static abstract (TState State, TEffect Effect) Initialize(TParameters parameters);
    static abstract (TState State, TEffect Effect) Transition(TState state, TEvent @event);
}

Two methods. Zero dependencies. The rest is runtime. Use Unit as TParameters for automata that require no initialization parameters.

Example: Counter

using Picea;

// Pure domain logic — no framework imports, no infrastructure
public record CounterState(int Count);

public interface CounterEvent
{
    record struct Increment : CounterEvent;
    record struct Decrement : CounterEvent;
}

public interface CounterEffect
{
    record struct None : CounterEffect;
}

public class Counter : Automaton<CounterState, CounterEvent, CounterEffect, Unit>
{
    public static (CounterState, CounterEffect) Initialize(Unit _) =>
        (new CounterState(0), new CounterEffect.None());

    public static (CounterState, CounterEffect) Transition(CounterState state, CounterEvent @event) =>
        @event switch
        {
            CounterEvent.Increment => (state with { Count = state.Count + 1 }, new CounterEffect.None()),
            CounterEvent.Decrement => (state with { Count = state.Count - 1 }, new CounterEffect.None()),
            _ => throw new UnreachableException()
        };
}

This single definition can drive an MVU runtime, an event-sourced aggregate, or a mailbox actor.

The Shared Runtime

The AutomatonRuntime executes the loop: dispatch → transition → observe → interpret, parameterized by two extension points:

Extension Point Signature Purpose
Observer (State, Event, Effect) → ValueTask<Result<Unit, PipelineError>> See each transition triple (render, persist, log)
Interpreter Effect → ValueTask<Result<Event[], PipelineError>> Convert effects to feedback events
// Observer: sees each (state, event, effect) triple after transition
public delegate ValueTask<Result<Unit, PipelineError>> Observer<in TState, in TEvent, in TEffect>(
    TState state, TEvent @event, TEffect effect);

// Interpreter: converts effects to feedback events
public delegate ValueTask<Result<TEvent[], PipelineError>> Interpreter<in TEffect, TEvent>(TEffect effect);

Errors propagate as Result values through the pipeline — not as exceptions.

var runtime = await AutomatonRuntime<Counter, CounterState, CounterEvent, CounterEffect, Unit>
    .Start(
        default,
        observer: (state, @event, effect) =>
        {
            Console.WriteLine($"{@event} → {state}");
            return PipelineResult.Ok;
        },
        interpreter: _ => InterpreterResult<CounterEvent>.Empty);

await runtime.Dispatch(new CounterEvent.Increment());
// Prints: Increment → CounterState { Count = 1 }

Production Guarantees

Property Guarantee
Thread safety All public mutating methods are serialized via SemaphoreSlim. Concurrent callers are queued, never interleaved. Pass threadSafe: false for single-threaded scenarios (actors, UI loops).
Cancellation All async methods accept CancellationToken.
Feedback depth Interpreter feedback loops are bounded (max 64 depth). Runaway cycles throw InvalidOperationException.
Error propagation Observer and Interpreter return Result<T, PipelineError> — errors are values, not exceptions.

Observer Composition

Observers compose with monadic combinators:

// Sequential (short-circuits on error)
var pipeline = persistObserver.Then(logObserver).Then(metricsObserver);

// Conditional
var heaterOnly = logObserver.Where((_, e, _) => e is HeaterTurnedOn or HeaterTurnedOff);

// Error recovery
var resilient = persistObserver.Catch(err => Result<Unit, PipelineError>.Ok(Unit.Value));

// Both run regardless of individual failures
var both = persistObserver.Combine(notifier);

Building Custom Runtimes

The AutomatonRuntime is the building block for specialized runtimes. Each runtime is just specific Observer and Interpreter wiring:

Runtime Pattern Observer Interpreter
MVU Render the new state Execute effects, return feedback events
Event Sourcing Append event to store No-op (empty)
Actor No-op (state is internal) Execute effect with self-reference

For combining multiple automata into a single runtime, see Composition.

The Decider — Command Validation

The Decider pattern (Chassaing, 2021) adds a command validation layer to the Automaton. It separates intent (commands) from facts (events):

Command → Decide(state, command) → Result<Events, Error> → Transition(state, event) → (State', Effect)

A Decider is an Automaton that also validates commands:

public interface Decider<TState, TCommand, TEvent, TEffect, TError, TParameters>
    : Automaton<TState, TEvent, TEffect, TParameters>
{
    static abstract Result<TEvent[], TError> Decide(TState state, TCommand command);
    static virtual bool IsTerminal(TState state) => false;
}

Together with the Automaton's Initialize and Transition, this gives the seven elements of the Decider pattern:

Element Provided by Method
Command type Type parameter TCommand
Event type Type parameter TEvent
State type Type parameter TState
Initial state Automaton Initialize(parameters)
Decide Decider Decide(state, command)
Evolve Automaton Transition(state, event)
Is terminal Decider IsTerminal(state)

Example: Bounded Counter

public class Counter
    : Decider<CounterState, CounterCommand, CounterEvent, CounterEffect, CounterError, Unit>
{
    public const int MaxCount = 100;

    public static (CounterState, CounterEffect) Initialize(Unit _) =>
        (new CounterState(0), new CounterEffect.None());

    public static Result<CounterEvent[], CounterError> Decide(
        CounterState state, CounterCommand command) =>
        command switch
        {
            CounterCommand.Add(var n) when state.Count + n > MaxCount =>
                Result<CounterEvent[], CounterError>
                    .Err(new CounterError.Overflow(state.Count, n, MaxCount)),

            CounterCommand.Add(var n) when n >= 0 =>
                Result<CounterEvent[], CounterError>
                    .Ok(Enumerable.Repeat<CounterEvent>(
                        new CounterEvent.Increment(), n).ToArray()),

            // ... Transition remains unchanged
        };

    public static (CounterState, CounterEffect) Transition(
        CounterState state, CounterEvent @event) =>
        @event switch
        {
            CounterEvent.Increment => (state with { Count = state.Count + 1 }, new CounterEffect.None()),
            CounterEvent.Decrement => (state with { Count = state.Count - 1 }, new CounterEffect.None()),
            _ => throw new UnreachableException()
        };
}

DecidingRuntime

The DecidingRuntime wraps AutomatonRuntime and adds Handle(command):

var runtime = await DecidingRuntime<Counter, CounterState, CounterCommand,
    CounterEvent, CounterEffect, CounterError, Unit>.Start(default, observer, interpreter);

// Valid command → events dispatched, state updated
var result = await runtime.Handle(new CounterCommand.Add(5));
// result is Ok(CounterState { Count = 5 })

// Invalid command → error returned, state unchanged
var overflow = await runtime.Handle(new CounterCommand.Add(200));
// overflow is Err(CounterError.Overflow { Current = 5, Amount = 200, Max = 100 })
// runtime.State.Count is still 5

The entire Handle operation — Decide + all Dispatches — executes under a single lock acquisition, preventing TOCTOU races.

Since Decider<...> : Automaton<...>, upgrading is non-breaking — all existing runtimes continue to work.

Result Type

Result<TSuccess, TError> is a readonly struct discriminated union — either Ok(value) or Err(error). Zero heap allocation per result.

var result = Counter.Decide(state, command);

// Pattern matching
var message = result.IsOk
    ? $"Produced {result.Value.Length} events"
    : $"Rejected: {result.Error}";

// LINQ query syntax (railway-oriented programming)
var final =
    from events in Counter.Decide(state, addCmd)
    from count in Result<int, CounterError>.Ok(events.Length)
    select $"{count} events produced";

// Fluent API — Functor (Map), Monad (Bind), Bifunctor (MapError)
result.Map(events => events.Length)
      .Bind(count => count > 0
          ? Result<string, CounterError>.Ok($"{count} events")
          : Result<string, CounterError>.Err(new CounterError.AlreadyAtZero()));

Observability — OpenTelemetry Tracing

The runtime emits distributed tracing spans via System.Diagnostics.ActivitySource — zero external dependencies, compatible with any OpenTelemetry collector.

Enabling Tracing

Register the source name with your telemetry pipeline:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing.AddSource(AutomatonDiagnostics.SourceName));

When no listener is registered, instrumentation has near-zero overhead (StartActivity() returns null).

Span Coverage

Span Name Tags
Automaton.Start automaton.type, automaton.state.type
Automaton.Dispatch automaton.type, automaton.event.type
Automaton.InterpretEffect automaton.type, automaton.effect.type
Automaton.Decider.Start automaton.type, automaton.state.type
Automaton.Decider.Handle automaton.type, automaton.command.type, automaton.result, automaton.error.type

Command rejections set automaton.result = "error" but use ActivityStatusCode.Ok — a rejected command is a correct business outcome, not a fault.

The Proof: It's All the Same Fold

var events = new CounterEvent[] { new Increment(), new Increment(), new Decrement() };
var (seed, _) = Counter.Initialize(default);

var finalState = events.Aggregate(seed, (state, @event) =>
    Counter.Transition(state, @event).State);

// finalState.Count == 1

MVU, Event Sourcing, and the Actor Model are all left folds over an event stream. The runtime is the variable. The transition function is the invariant.

Why This Matters

Traditional approach Picea approach
Domain logic coupled to UI framework Domain logic is pure — zero dependencies
Rewrite business rules for each tier Write once, run in browser + server + actor
Test through infrastructure Test the transition function directly
Framework dictates architecture Math dictates architecture, framework is pluggable
Validation scattered across layers Validation is a pure function on the Decider

Architecture

┌─────────────────────────────────────────────────────┐
│           Automaton<S, E, F, P>                      │
│  Initialize(parameters) + Transition(state, event)   │
└──────────────────────────┴──────────────────────────┘
                           │
            ┌──────────────┴──────────────┐
            │  Decider<S, C, E, F, Err, P> │
            │  Decide(state, command)       │
            │  IsTerminal(state)            │
            └──────────────┴──────────────┘
                           │
           ┌───────────────┴───────────────┐
           │  AutomatonRuntime<A,S,E,F,P>   │
           │  Observer + Interpreter        │
           │  AutomatonDiagnostics          │
           └─────┴──────────────────┴──────┘
                 │                  │
           ┌─────┴──────┐  ┌───────┴────────┐
           │  Your MVU   │  │  Your ES /     │
           │  Runtime    │  │  Actor / ...   │
           └─────────────┘  └────────────────┘

Multiple automata can be composed into a single automaton via product state and sum events — the automata-theoretic product construction.

What's in the Box

Type Purpose
Automaton<TState, TEvent, TEffect, TParameters> Mealy machine interface (Initialize + Transition)
AutomatonRuntime<TAutomaton, TState, TEvent, TEffect, TParameters> Thread-safe async runtime (dispatch → transition → observe → interpret)
Observer<TState, TEvent, TEffect> Transition observer delegate (→ Result<Unit, PipelineError>)
Interpreter<TEffect, TEvent> Effect interpreter delegate (→ Result<Event[], PipelineError>)
ObserverExtensions Monadic combinators: Then, Where, Select, Catch, Combine
InterpreterExtensions Monadic combinators: Then, Where, Select, Catch
Decider<TState, TCommand, TEvent, TEffect, TError, TParameters> Command validation interface (Decide + IsTerminal)
DecidingRuntime<...> Command-validating runtime wrapper with atomic Handle
Result<TSuccess, TError> readonly struct discriminated union with Map, Bind, MapError, LINQ syntax
PipelineError Structured error for Observer/Interpreter pipelines
PipelineResult Pre-allocated Ok value for zero-alloc observer fast path
InterpreterResult<TEvent> Pre-allocated Empty value for zero-alloc interpreter fast path
AutomatonDiagnostics OpenTelemetry-compatible tracing (ActivitySource)

The Picea Ecosystem

Package Description Repo
Picea Core kernel, runtime, Decider, Result, diagnostics picea/picea
Picea.Abies MVU framework for Blazor (Browser + Server) picea/abies
Picea.Glauca Event Sourcing patterns (AggregateRunner, EventStore) picea/glauca
Picea.Rubens Actor model patterns (Actor, Address, Envelope) picea/rubens
Picea.Mariana Resilience patterns (Retry, Circuit Breaker, Rate Limiter) picea/mariana

Benchmarks

Continuous benchmarks run on every push to main via BenchmarkDotNet. Performance regressions exceeding 150% automatically fail the build.

Documentation

Full documentation with concepts, tutorials, how-to guides, and API reference:

  • Concepts — The Kernel, The Runtime, The Decider, Composition, Glossary
  • Tutorials — step-by-step guides for building systems with the kernel
  • How-To Guides — Observer composition, testing, error handling, custom runtimes
  • API Reference — complete type and method documentation
  • Architecture Decision Records — design rationale with mathematical grounding

License

Apache 2.0 — Copyright 2025-2026 Maurice Peters

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on Picea:

Package Downloads
Picea.Abies

Package Description

Picea.Glauca

Event Sourcing patterns modeled as Mealy machine automata — Aggregate runners with optimistic concurrency, conflict resolution, Saga orchestration, Projections, and pluggable EventStore abstraction. Built on the Picea kernel with Result-based error handling and OpenTelemetry instrumentation.

Picea.Rubens

Actor model (Hewitt 1973) built on the Picea kernel — typed mailbox actors with unforgeable address capabilities, one-shot reply actors, envelope-based request-reply. All actors are Deciders: they validate commands against state, produce events, and evolve via transitions.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.34-rc-0002 240 3/30/2026
1.0.33-rc-0002 1,292 3/16/2026
1.0.32-rc-0002 34 3/16/2026
1.0.31-rc-0002 43 3/16/2026
1.0.30-rc-0002 40 3/16/2026
1.0.29-rc-0002 44 3/16/2026
1.0.28-rc-0002 184 3/14/2026
1.0.27-rc-0002 1,035 3/13/2026
1.0.26-rc-0001 41 3/13/2026
1.0.22-rc-0001 253 3/12/2026
1.0.21-rc-0001 45 3/12/2026
1.0.20-rc-0001 45 3/12/2026
1.0.19-rc-0001 329 3/9/2026
1.0.18-rc-0001 41 3/9/2026
1.0.17-rc-0001 41 3/9/2026
1.0.16-rc-0001 208 3/9/2026