SoftwareDriven.Blockly.Code
1.9.0
dotnet add package SoftwareDriven.Blockly.Code --version 1.9.0
NuGet\Install-Package SoftwareDriven.Blockly.Code -Version 1.9.0
<PackageReference Include="SoftwareDriven.Blockly.Code" Version="1.9.0" />
<PackageVersion Include="SoftwareDriven.Blockly.Code" Version="1.9.0" />
<PackageReference Include="SoftwareDriven.Blockly.Code" />
paket add SoftwareDriven.Blockly.Code --version 1.9.0
#r "nuget: SoftwareDriven.Blockly.Code, 1.9.0"
#:package SoftwareDriven.Blockly.Code@1.9.0
#addin nuget:?package=SoftwareDriven.Blockly.Code&version=1.9.0
#tool nuget:?package=SoftwareDriven.Blockly.Code&version=1.9.0
SoftwareDriven.Blockly.Code
A Blockly based code engine for .NET.
Blockly is a JavaScript library for building
visual programming editors. This library takes the Blockly XML produced by such an
editor and executes it server-side in C# — Blockly's own JavaScript code generators
are not used. The XML is parsed into a tree of Statement objects that a custom
interpreter runs.
The package contains no UI. To embed the editor itself into a Blazor application, use SoftwareDriven.Blockly.Blazor.
Installation
dotnet add package SoftwareDriven.Blockly.Code
Target framework: net10.0.
Quick start
using SoftwareDriven.Blockly.Code;
using SoftwareDriven.Blockly.Code.Parser;
using SoftwareDriven.Blockly.Code.Statements;
// 1. Parse the Blockly XML into an executable program.
var reader = new BlocklyXmlReader();
var control = reader.ReadFile("Program.xml", new BlockControlConfig()
{
OutputTopics = BlockOutputTopic.TextPrint | BlockOutputTopic.Errors,
DefaultOutput = BlockDefaultOutput.Console,
});
if (control == null)
return; // The file could not be read.
// 2. Run it.
var result = control.Run(new RunContext() { Caller = "MyApp" });
// 3. Evaluate the result and read back the variables.
// Error: a faulty program. Exception: a guard was hit (MaxStackDepth / MaxLoopCycles).
if (result.Type is ExecutionResultType.Error or ExecutionResultType.Exception)
Console.WriteLine(result.Message);
var answer = control.GetVariableByName("answer");
Console.WriteLine(answer?.Value);
ReadStream(Stream, BlockControlConfig?) is the equivalent overload for XML coming from
a database or from the Blazor editor's Export():
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(workspaceXml));
var control = reader.ReadStream(stream, config);
Writing the program back to XML — e.g. to feed it into the editor again:
var writer = new BlocklyXmlWriter();
writer.WriteFile("Program.xml", control);
using var output = new MemoryStream();
writer.WriteStream(output, control);
var xml = Encoding.UTF8.GetString(output.ToArray());
Configuration
BlockControlConfig is passed to the reader and is available afterwards as
control.Config.
| Property | Description |
|---|---|
Name |
A name for the program, used in the log output. |
OutputTopics |
Flags selecting what is written to the output: Errors, TextPrint, LogExecutionLogic, LogSetVariable, MemberAccess. |
DefaultOutput |
Where the output goes: Debug, Console or Logger. |
Logger |
The ILogger used when DefaultOutput is Logger. |
MaxStackDepth |
Guard against runaway recursion (default 1000). |
MaxLoopCycles |
Guard against endless loops (default 1000). |
FormattedJson |
Indents the JSON values produced by the engine. |
BlockRepository |
The IBlockRepository providing custom blocks. |
TypeRegistry |
The IBlockTypeRegistry describing custom object types. |
HttpClient |
The client used by the API call blocks. |
Keep the MaxStackDepth / MaxLoopCycles guards — they prevent a faulty user program
from taking down the host process.
Values and variables
Values are stringly typed. A TypedValue holds a string? Value plus a string? Type
naming a KnownType (Boolean, String, Number, Array, Colour, DateTime,
TimeSpan, Dictionary, Object) or a registered custom type. BlockVariable adds a
name to that pair.
Numbers and dates are always parsed and formatted with CultureInfo.InvariantCulture.
// Provide input before the run ...
control.CreateOrSetVariable("input", "42", KnownType.Number);
control.Run(new RunContext());
// ... and read the output afterwards.
foreach (var variable in control.Variables)
Console.WriteLine($"{variable.Name} = {variable.Value} ({variable.Type})");
Further members of interest on BlockControl: Main (the top level function),
Functions, GetFunctionByName(), GetVariableById(), SetVariableByName() and
Copy() to run the same program several times in parallel.
Custom object types
The object members used by the GetMember / SetMember / MemberAccess blocks are
resolved through an IBlockTypeRegistry. Types can be declared explicitly, taken from a
.NET type by reflection, or deserialized from JSON.
var registry = new BlockTypeRegistry();
// The built-in types (Boolean, String, Number, ...).
registry.AddKnownTypes();
// A hand written type.
registry.GetOrAddType("Customer", "Customer")
.AddMember("Name", nameof(KnownType.String))
.AddMember("Orders", nameof(KnownType.Array));
// A type derived from a .NET class by reflection.
registry.GetOrAddReflectedType<Order>();
config.TypeRegistry = registry;
A registry can also be loaded from JSON and layered with others:
var fromJson = JsonSerializer.Deserialize<BlockTypeRegistry>(json);
var composite = new CompositeTypeRegistry();
composite.Registries.Add(registry);
composite.Registries.Add(fromJson!);
Values of registered types are JSON documents. The registry offers helpers to work with them without leaving the string world:
var value = registry.CreateValueOfRegisteredType("Customer")?.ToString();
registry.SetNestedValue("Customer", "Name", value, "\"Doe\"", out value);
registry.AppendNestedValue("Customer", "Orders", value!, "{ \"Id\": 1 }", out value);
registry.GetElementsOfNestedArray("Customer", "Orders", value, "Order", out var orders);
Custom blocks
All standard Blockly blocks are handled by the engine itself. An IBlockRepository only
has to deal with custom block types; returning null from CreateStatementFromBlock
defers to the built-in handling.
public class MyRepository : IBlockRepository
{
public string? BlocksJson => myBlockDefinitionsJson; // Blockly block JSON for the editor.
public Toolbox Toolbox { get; } = new();
public Statement? CreateStatementFromBlock(BlockControl control, XmlNode node,
string id, string type, BlocklyXmlReader reader)
{
if (type != "my_block")
return null; // Not ours - let the engine handle it.
return new MyStatement()
{
ID = id,
Input = reader.TryCreateValueStatementFromBlock(control, node.ChildNodes.Cast<XmlNode>(), "INPUT"),
};
}
public string? CreateBlockFromStatement(BlockControl control, XmlDocument doc,
XmlElement node, Statement statement, BlocklyXmlWriter writer)
{
if (statement is not MyStatement my)
return null;
writer.CreateValue(control, doc, node, my.Input, "INPUT");
return "my_block";
}
}
The matching statement derives from Statement, or from ValueStatement if it yields a
value. Execute is called repeatedly: return ExecutionResult.Next(child) to have a
child evaluated first and ExecutionResult.Done() when finished. On the following call
continueFromChildId names the child that has just completed.
public class MyStatement : ValueStatement
{
public ValueStatement? Input { get; set; }
public override IEnumerable<Statement> Children
{
get { if (Input != null) yield return Input; }
}
public override ExecutionResult Execute(BlockControl control, RunContext context, string continueFromChildId)
{
if (string.IsNullOrWhiteSpace(continueFromChildId) && (Input != null))
return ExecutionResult.Next(Input); // Evaluate the input first.
Result = new TypedValue()
{
Value = Input?.Result?.Value?.ToUpper(),
Type = nameof(KnownType.String),
};
return ExecutionResult.Done();
}
public override Statement Copy() => new MyStatement() { ID = ID, Input = Input?.CopyV() };
}
Use CompositeRepository to combine several repositories and DefaultRepository to
serve blocks.json / toolbox.json from a directory while deferring all execution to
the built-in blocks:
var repository = new CompositeRepository();
var defaultRepository = new DefaultRepository();
defaultRepository.Init("Content"); // Reads Content/blocks.json and Content/toolbox.json.
repository.Repositories.Add(defaultRepository);
repository.Repositories.Add(new MyRepository());
config.BlockRepository = repository;
Toolbox
The Editor namespace models the Blockly toolbox as C# objects, so the same definition
can be handed to the editor and kept under source control. ToolboxFactory provides the
built-in categories as extension methods.
using SoftwareDriven.Blockly.Code.Editor;
var toolbox = new Toolbox();
toolbox.AddLogicCategory()
.AddLoopsCategory()
.AddMathCategory()
.AddTextCategory()
.AddSeparator()
.AddVariablesCategory()
.AddFunctionsCategory()
.AddCategory("My blocks", category =>
{
category.Colour = "#00FFFF";
category.Contents = [new Block("my_block")];
});
toolbox.HideCategory(ToolboxFactory.ApiCategoryName);
toolbox.DisableBlock("text_print");
Suspending and resuming a run
Suspend/resume is a first-class feature: a statement may return ExecutionResult.Wait()
when it depends on something external. The engine then keeps its exact position in the
program: StatementIdStack, LastChildId and LastItemState (the state of the waiting
statement and of the loops around it). Together with Variables this is all a host has to
persist. The run can be continued by the same BlockControl - or by a control parsed again
from the same XML, e.g. after a restart of the host.
var result = control.Run(new RunContext());
if (result.Type == ExecutionResultType.Wait)
{
// Persist the position and the variables.
var stack = control.StatementIdStack.Reverse().ToList();
var lastChildId = control.LastChildId;
var lastItemState = control.LastItemState;
var variables = control.Variables;
}
// Later, possibly in another process: parse the same XML again and continue.
var resumed = reader.ReadStream(stream, config)!;
resumed.StatementIdStack = new Stack<string>(stack);
resumed.LastChildId = lastChildId;
resumed.LastItemState = lastItemState;
resumed.Variables = variables;
resumed.Run(new RunContext());
A statement that waits should implement the pair SerializeStateAtWait() /
DeserializeStateAtContinue() so that its own counters and intermediate values (including
the results of inputs it has already evaluated) survive the interruption. Statement IDs are
the addresses of a suspended run: a custom statement that creates child statements itself
should derive their IDs from its own ID, which comes from the XML.
| Product | Versions 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. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on SoftwareDriven.Blockly.Code:
| Package | Downloads |
|---|---|
|
SoftwareDriven.Blockly.Blazor
SoftwareDriven.Blockly.Blazor is a blazor razor component with an embedded blockly editor. Blockly is a JavaScript library for building visual programming editors. https://developers.google.com/blockly |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.9.0 | 45 | 9/17/2026 |
| 1.8.0 | 129 | 8/25/2026 |
| 1.7.8 | 653 | 4/7/2025 |
| 1.7.7 | 462 | 3/6/2025 |
| 1.7.6 | 393 | 3/6/2025 |
| 1.7.4 | 395 | 3/5/2025 |
| 1.7.3 | 414 | 3/3/2025 |
| 1.7.1 | 361 | 12/5/2024 |
| 1.7.0 | 381 | 9/26/2024 |
| 1.6.2 | 423 | 3/6/2024 |
| 1.6.1 | 352 | 1/23/2024 |
| 1.6.0 | 354 | 1/22/2024 |
| 1.5.2 | 424 | 12/21/2023 |
| 1.5.1 | 321 | 12/21/2023 |
| 1.4.0 | 299 | 11/27/2023 |
| 1.2.4 | 906 | 2/14/2022 |