C# WASM Nodes
C# brings the .NET ecosystem to Flow-Like WASM nodes. The template uses the experimental WASI workload for .NET 10, with WIT bindings handled automatically by the MSBuild integration. A high-level FlowLikeWasmSdk NuGet package provides ergonomic Context, NodeDefinition, and ExecutionResult types.
Prerequisites
Section titled “Prerequisites”- .NET 10 SDK
- WASI experimental workload
dotnet workload install wasi-experimentalThe WIT bindings are processed at build time by the SDK — no separate wit-bindgen or wasm-tools install required.
Project Structure
Section titled “Project Structure”wasm-node-csharp/├── FlowLikeWasmNode.csproj # Project file (wasi-wasm target)├── Node.cs # Node definition & run logic├── Program.cs # WASM entry point / CLI dispatcher├── flow-like.toml # Flow-Like package manifest└── mise.toml # Build tasksThe WIT file is copied from the monorepo at build time (mise run build runs wit-copy first).
Project File
Section titled “Project File”<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <OutputType>Exe</OutputType> <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault> </PropertyGroup> <ItemGroup> <PackageReference Include="FlowLikeWasmSdk" Version="1.0.0" /> </ItemGroup> <ItemGroup> <Wit Include="wit/flow-like-node.wit" World="flow-like-node" /> </ItemGroup></Project>The <Wit> item tells the WASI SDK to process the WIT file and generate bindings automatically.
Quick Start
Section titled “Quick Start”Node Definition
Section titled “Node Definition”Define your node’s metadata, pins, and permissions in Node.cs:
using FlowLike.Wasm.Sdk;
namespace FlowLike.Wasm.Node;
public static class CustomNode{ public static NodeDefinition GetDefinition() { var nd = new NodeDefinition( name: "my_custom_node_csharp", friendlyName: "My Custom Node", description: "A template WASM node", category: "Custom/WASM" ); nd.AddPermission("streaming");
nd.AddPin(PinDefinition.InputExec("exec")); nd.AddPin(PinDefinition.InputPin("input_text", PinType.String, defaultValue: "")); nd.AddPin(PinDefinition.InputPin("multiplier", PinType.I64, defaultValue: 1));
nd.AddPin(PinDefinition.OutputExec("exec_out")); nd.AddPin(PinDefinition.OutputPin("output_text", PinType.String)); nd.AddPin(PinDefinition.OutputPin("char_count", PinType.I64));
return nd; }
public static ExecutionResult Run(Context ctx) { var inputText = ctx.GetString("input_text", "") ?? ""; var multiplier = ctx.GetI64("multiplier", 1) ?? 1;
ctx.Debug($"Processing: '{inputText}' x {multiplier}");
var repeated = multiplier > 0 ? string.Concat(Enumerable.Repeat(inputText, (int)multiplier)) : "";
ctx.StreamText($"Generated {repeated.Length} characters");
ctx.SetOutput("output_text", repeated); ctx.SetOutput("char_count", repeated.Length);
return ctx.Success(); }}Entry Point
Section titled “Entry Point”Program.cs dispatches WIT export calls. It also supports CLI invocation for local testing:
using FlowLike.Wasm.Sdk;using FlowLike.Wasm.Node;
var cliArgs = Environment.GetCommandLineArgs();if (cliArgs.Length >= 2){ var command = cliArgs[1]; if (string.Equals(command, "get-node", StringComparison.OrdinalIgnoreCase)) { Console.Write(WitExports.GetNode()); return; } if (string.Equals(command, "run", StringComparison.OrdinalIgnoreCase)) { var inputJson = cliArgs.Length >= 3 ? cliArgs[2] : Console.In.ReadToEnd(); Console.Write(WitExports.Run(inputJson ?? "{}")); return; }}
public static class WitExports{ public static string GetNode() { var definition = CustomNode.GetDefinition(); return Json.Serialize(new[] { definition.ToDictionary() }); }
public static string GetNodes() => GetNode();
public static string Run(string inputJson) { var ctx = Context.FromJson(inputJson); var result = CustomNode.Run(ctx); return result.ToJson(); }
public static int GetAbiVersion() => 1;}SDK API
Section titled “SDK API”// Read inputsctx.GetString("pin_name", defaultValue) // -> string?ctx.GetI64("pin_name", defaultValue) // -> long?ctx.GetF64("pin_name", defaultValue) // -> double?ctx.GetBool("pin_name", defaultValue) // -> bool?
// Write outputsctx.SetOutput("pin_name", value)
// Loggingctx.Debug("message")ctx.Info("message")ctx.Warn("message")ctx.Error("message")
// Streamingctx.StreamText("partial output")
// Execution controlctx.Success() // -> ExecutionResult (activates "exec_out")ctx.Fail("reason") // -> ExecutionResult with errorThe template uses mise for task orchestration:
# One-time setup: install WASI workloadmise run setup
# Build (copies WIT, then publishes as single-file WASM bundle)mise run buildThe build command runs:
dotnet publish -c Release \ /p:WasmSingleFileBundle=true \ /p:WasiClangLinkOptimizationFlag=-O0 \ /p:WasiClangCompileOptimizationFlag=-O0 \ /p:WasiBitcodeCompileOptimizationFlag=-O0Output: bin/Release/net10.0/wasi-wasm/AppBundle/FlowLikeWasmNode.wasm
Manual Build (without mise)
Section titled “Manual Build (without mise)”# Copy WIT filemkdir -p witcp ../../packages/wasm/wit/flow-like-node.wit wit/
# Install workload + restoredotnet workload install wasi-experimentaldotnet restore
# Publishdotnet publish -c Release /p:WasmSingleFileBundle=trueTesting
Section titled “Testing”mise run test # runs dotnet testmise run clean # removes bin/, obj/, wit/Related
Section titled “Related”- Overview — How WASM nodes work
- Package Manifest — Full manifest reference
- Rust WASM Nodes — Recommended language with SDK macros