C# Target

C# Target

Generate high-performance state machines for .NET applications.

Table of Contents

  1. Quick Start
  2. Code Generation
  3. Datamodels
  4. Runtime Integration
  5. Event Handling
  6. Invoke Support
  7. Unity Integration
  8. W3C Compliance

Quick Start

1. Generate C# Code

bash
# Generate from SCXML
scxml-gen traffic.scxml -t csharp -o TrafficLight.cs --namespace MyApp.StateMachines

2. Build the Generated Project

The generator produces a ready-to-build .NET 8 project — no NuGet packages to configure manually.

bash
cd output/
dotnet build    # restores Jint automatically
dotnet run      # runs Program.cs demo

Generated project layout:

output/
├── TrafficLight.cs           # Generated state machine
├── TrafficLight.csproj       # Main project (net8.0)
├── TrafficLight.sln          # Solution file
├── Program.cs                # Ready-to-run demo
└── ScxmlGen.Runtime/         # Bundled runtime source
    ├── TranspiledStateMachine.cs
    ├── Executor/
    ├── Trace/
    └── ...

Code-only mode: Use --code-only to generate just the .cs file when integrating into an existing project that already references ScxmlGen.Runtime.

3. Use the State Machine

csharp
using MyApp.StateMachines;
using ScxmlGen.Runtime.Executor;

var machine = new TrafficLight();

// ContinuousExecutor handles delayed events autonomously — no manual PumpEvents() needed
using var executor = new ContinuousExecutor(machine);
executor.Start();

await executor.SendAsync("timer");
Console.WriteLine(machine.IsInState("green"));  // true

Code Generation

CLI Options

bash
scxml-gen input.scxml -t csharp [options]
Option Description Example
-o, --output Output file path -o Output.cs
--namespace C# namespace --namespace MyApp.Machines
--code-only Generate only the .cs file (no project, no runtime) --code-only
--bundle-auto Auto-bundle invoke children --bundle-auto
--bundle Manually bundle children --bundle child.scxml

Generated Code Structure

csharp
using System;
using System.Collections.Generic;
using ScxmlGen.Runtime;

namespace MyNamespace
{
    public class MyStateMachine : TranspiledStateMachine
    {
        // State constants
        public const int S_IDLE = 0;
        public const int S_RUNNING = 1;

        // Event constants (O(1) integer dispatch)
        public const int EVT_START = 1;
        public const int EVT_STOP = 2;
        public const int EVT_UNKNOWN = 99999;

        // Datamodel fields (native-csharp)
        protected int counter;
        protected List<string> items;

        public MyStateMachine() : base()
        {
            Name = "MyStateMachine";
            InitializeStates();
        }

        // State entry/exit methods
        protected virtual void OnEntryIdle() { ... }
        protected virtual void OnExitIdle() { ... }

        // Transition handling
        protected override void ProcessEvent(string eventName) { ... }
    }
}

Datamodels

ECMAScript (Jint)

Full JavaScript expression support via the Jint engine.

xml
<scxml datamodel="ecmascript" initial="start">
    <datamodel>
        <data id="user" expr="{name: 'Alice', age: 30}"/>
        <data id="items" expr="[]"/>
    </datamodel>

    <state id="start">
        <onentry>
            <script>
                items.push(user.name);
                user.age++;
            </script>
        </onentry>
        <transition cond="items.length > 0" target="done"/>
    </state>

    <final id="done"/>
</scxml>

Features:

  • Full ECMAScript 5.1+ support
  • JSON object literals
  • Array methods (push, pop, map, filter)
  • XML DOM support for W3C compliance
  • 100% W3C conformance (183/183 tests)

Native-CSharp

Compile-time C# code generation with full type safety.

xml
<scxml datamodel="native-csharp" initial="init">
    <datamodel>
        <data id="counter" type="int" expr="0"/>
        <data id="items" type="List&lt;string&gt;" expr="new List&lt;string&gt;()"/>
        <data id="config" type="Dictionary&lt;string, object&gt;"
              expr="new Dictionary&lt;string, object&gt;()"/>
    </datamodel>

    <state id="init">
        <onentry>
            <script>
                // Direct C# code
                for (int i = 0; i &lt; 5; i++) {
                    items.Add($"Item {i}");
                }

                // LINQ
                var filtered = items.Where(x => x.Contains("2")).ToList();

                // .NET APIs
                config["timestamp"] = DateTime.UtcNow;
            </script>
        </onentry>
        <transition cond="items.Count >= 5" target="done"/>
    </state>

    <final id="done"/>
</scxml>

Supported Types:

  • Primitives: int, long, double, float, bool, string
  • Collections: List<T>, Dictionary<K,V>, HashSet<T>
  • Nullable: int?, string?
  • Custom classes (must be fully qualified)

Null Datamodel

Minimal state machine without expression evaluation.

xml
<scxml datamodel="null" initial="idle">
    <state id="idle">
        <transition event="start" target="running"/>
    </state>
    <state id="running">
        <transition event="stop" target="idle"/>
    </state>
</scxml>

Only In() predicate is supported for conditions.


Runtime Integration

TranspiledStateMachine Base Class

csharp
public interface IScxmlStateMachine
{
    // Properties
    string Name { get; }
    string SessionId { get; }
    bool IsFinished { get; }   // true once a top-level <final> is reached
    bool IsStarted { get; }
    IReadOnlySet<string> ActiveStates { get; }
    IReadOnlySet<string> FinalStates { get; }

    // Core methods
    void Start();
    void Start(IReadOnlyDictionary<string, object?>? initData);
    void Send(string eventName, IReadOnlyDictionary<string, object?>? data = null);
    void Send(Event evt);
    bool IsInState(string stateId);
    void PumpEvents();   // drain pending timer-fired events (manual use; executor does this automatically)

    // Data model access
    object? GetVariable(string name);
    void SetVariable(string name, object? value);
}

Event Dispatch Methods

csharp
var machine = new TrafficLight();
machine.Start();

// String-based (convenient)
machine.Send("timer");

// Integer-based (O(1) performance - use EVT_* constants)
machine.SendById(TrafficLight.EVT_TIMER);

// With event data
machine.Send("submit", new Dictionary<string, object?> {
    { "username", "alice" }
});
machine.SendById(TrafficLight.EVT_SUBMIT, new Dictionary<string, object?> {
    { "username", "alice" }
});

Trace API

csharp
using ScxmlGen.Runtime.Trace;

// Console debugging (create custom listener)
public class ConsoleTraceListener : TraceAdapter
{
    public override void OnStateEnter(string stateId, long timestampMicros)
        => Console.WriteLine({{content}}quot;Enter: {stateId}");

    public override void OnStateExit(string stateId, long timestampMicros)
        => Console.WriteLine({{content}}quot;Exit: {stateId}");
}

// JSONL file output
using var writer = new JsonlTraceWriter("trace.jsonl");
machine.SetTraceListener(writer);
machine.Start();
machine.Send("go");

// Invoke-aware tracing for parent/child machines
public class MyListener : InvokeAwareTraceAdapter
{
    public override void OnStateEnter(string stateId, long timestampMicros, string? invokeId)
    {
        if (invokeId != null)
            Console.WriteLine({{content}}quot;Child {invokeId} entered: {stateId}");
        else
            Console.WriteLine({{content}}quot;Parent entered: {stateId}");
    }
}

ITraceListener Interface

csharp
public interface ITraceListener
{
    void OnSessionStart(string sessionId, string scxmlName, string datamodel, long timestampMicros);
    void OnSessionEnd(string sessionId, IReadOnlySet<string> finalStates, long timestampMicros);
    void OnStateEnter(string stateId, long timestampMicros);
    void OnStateExit(string stateId, long timestampMicros);
    void OnTransitionFired(string? from, string to, string? eventName, long timestampMicros);
    void OnEventReceived(Event evt, long timestampMicros);
    void OnEventProcessed(Event evt, long timestampMicros);
    void OnVariableChanged(string name, object? oldValue, object? newValue, long timestampMicros);
    void OnActionExecute(string actionType, IReadOnlyDictionary<string, object?> details, long timestampMicros);
}

IInvokeAwareTraceListener Interface

Extended interface with invokeId for parent/child context:

csharp
public interface IInvokeAwareTraceListener : ITraceListener
{
    void OnStateEnter(string stateId, long timestampMicros, string? invokeId);
    void OnStateExit(string stateId, long timestampMicros, string? invokeId);
    // ... all methods with invokeId parameter
}

Executor API

using ScxmlGen.Runtime.Executor;

// ── ContinuousExecutor (recommended for production) ───────────────────────────
// Runs the machine on a dedicated background thread. Delayed events (<send delay="..."/>)
// are processed automatically via the wakeup listener — no manual PumpEvents() needed.
using var executor = new ContinuousExecutor(machine);
executor.Start();

// Send events (synchronous overload blocks until processed)
executor.Send("go");

// Async overload — awaitable, integrates with async/await code
await executor.SendAsync("go");
await executor.SendAsync("next");

// Wait for the machine to reach a <final> state
while (!machine.IsFinished)
    await Task.Delay(50);
// Dispose() stops the background thread cleanly

// ── RunToCompletionExecutor (simple, synchronous cases) ───────────────────────
// Processes each event synchronously on the caller's thread.
// Use for test harnesses or scenarios without delayed events.
var executor2 = new RunToCompletionExecutor(machine2);
executor2.Start();
executor2.Send("go");
Console.WriteLine(executor2.IsRunning);

IStateMachineExecutor Interface

csharp
public interface IStateMachineExecutor : IDisposable
{
    IScxmlStateMachine StateMachine { get; }
    bool IsRunning { get; }

    void Start();
    void Start(IReadOnlyDictionary<string, object?>? initData);

    void Send(string eventName);
    void Send(Event evt);
    void Send(string eventName, IReadOnlyDictionary<string, object?>? data);

    Task SendAsync(string eventName);
    Task SendAsync(Event evt);
    Task SendAsync(string eventName, IReadOnlyDictionary<string, object?>? data);

    void PumpEvents();
}

Event Data

csharp
// Directly on the machine
machine.Send("submit", new Dictionary<string, object?> {
    { "username", "alice" },
    { "password", "secret" }
});

// Via executor (thread-safe, awaitable)
await executor.SendAsync("submit", new Dictionary<string, object?> {
    { "username", "alice" },
    { "password", "secret" }
});

// Access in SCXML (ECMAScript)
<transition event="submit" cond="_event.data.username === 'alice'" target="authenticated"/>

State Inspection

csharp
// Check specific state
if (machine.IsInState("processing")) {
    Console.WriteLine("Still processing...");
}

// Get all active states (for parallel states)
foreach (var state in machine.ActiveStates) {
    Console.WriteLine({{content}}quot;Active: {state}");
}

// Check completion
if (machine.IsFinished) {
    Console.WriteLine({{content}}quot;Final states: {string.Join(", ", machine.FinalStates)}");
}

Event Introspection

csharp
IReadOnlySet<string> all = machine.GetAllEvents();
IReadOnlySet<string> enabled = machine.GetEnabledEvents();        // guard-aware
IReadOnlySet<string> forState = machine.GetEventsForState("s1");
IReadOnlySet<string> enabledFor = machine.GetEnabledEventsForState("s1");

Event Handling

Internal Events (Raise)

xml
<state id="validate">
    <onentry>
        <if cond="isValid">
            <raise event="valid"/>
        <else/>
            <raise event="invalid"/>
        </if>
    </onentry>
    <transition event="valid" target="success"/>
    <transition event="invalid" target="error"/>
</state>

Delayed Events (Send)

xml
<state id="waiting">
    <onentry>
        <send event="timeout" delay="5s"/>
    </onentry>
    <transition event="timeout" target="timedOut"/>
    <transition event="response" target="success"/>
</state>

Note: Delayed events use Timer internally. When wrapped in ContinuousExecutor, timer-fired events are processed automatically on the background thread — no manual PumpEvents() or async context required. For manual use without an executor, call machine.PumpEvents() periodically.

External Events

csharp
// From application code (directly on machine)
machine.Send("userInput", inputData);

// Via executor (thread-safe)
executor.Send("userInput", inputData);

// From another machine (via invoke)
parentMachine.Send("done.invoke.child1", resultData);

Invoke Support

Inline Child Machine

xml
<state id="processing">
    <invoke id="worker">
        <content>
            <scxml initial="work">
                <state id="work">
                    <onentry>
                        <send target="#_parent" event="progress"/>
                    </onentry>
                    <transition event="complete" target="done"/>
                </state>
                <final id="done">
                    <donedata>
                        <content expr="result"/>
                    </donedata>
                </final>
            </scxml>
        </content>
    </invoke>
    <transition event="done.invoke.worker" target="complete"/>
</state>

External Child Machine (Bundled)

bash
# Bundle child at build time
scxml-gen parent.scxml -t csharp -o Parent.cs --bundle child.scxml
xml
<!-- parent.scxml -->
<state id="processing">
    <invoke src="child.scxml" id="worker"/>
    <transition event="done.invoke.worker" target="complete"/>
</state>

Unity Integration

MonoBehaviour Wrapper

csharp
using UnityEngine;
using MyGame.StateMachines;

public class CharacterController : MonoBehaviour
{
    private CharacterStateMachine _machine;

    void Awake()
    {
        _machine = new CharacterStateMachine();
    }

    void Start()
    {
        _machine.Start();
    }

    void Update()
    {
        // Process input events
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _machine.Send("jump");
        }

        // React to state
        if (_machine.IsInState("airborne"))
        {
            ApplyGravity();
        }
    }

    void OnDestroy()
    {
        _machine.Stop();
    }
}

ScriptableObject State Machine

csharp
[CreateAssetMenu(menuName = "StateMachines/AI Behavior")]
public class AIBehaviorAsset : ScriptableObject
{
    public AIStateMachine CreateInstance()
    {
        return new AIStateMachine();
    }
}

W3C Compliance

The C# target achieves 100% W3C compliance across all supported datamodels.

Datamodel Tests Pass Rate
ECMAScript (Jint) 183/183 100%
Null 6/6 100%
Native-CSharp 11/11 100%
Total 200/200 100%

Running Tests

bash
cd scxml-csharp
dotnet test

Key Compliance Features

  • Full _event object support (name, type, origin, origintype, sendid, data, invokeid, raw)
  • XML DOM support for test557/test561 (getElementsByTagName, getAttribute, etc.)
  • System variables (_sessionid, _name, _ioprocessors)
  • Invoke with parent/child communication
  • History states (shallow and deep)
  • Parallel states with correct done.state events
  • Late binding support

Performance

Benchmarks

Operation ECMAScript Native-CSharp Null
State transitions/sec ~50,000 ~500,000 ~1,000,000
Memory per instance ~100KB ~1KB ~500B
Startup time ~10ms <1ms <1ms

Optimization Tips

  1. Use native-csharp for performance-critical applications
  2. Avoid ECMAScript in tight loops
  3. Reuse machine instances when possible
  4. Use null datamodel for simple event-driven workflows

See Also