Live demo — data resets daily at 03:00 UTC. Nothing you enter is saved. Server UI →

product: assert audience: test-developer authority: normative

.NET Runner SDK

The Assert runner DOES NOT build your project. You MUST run dotnet publish and commit the output to assemblies/ before the runner can load your DLLs. See reference/local-dev-commands.md for the exact command.

Project setup

Target framework: net10.0.

To return MeasurementPointBase objects (recommended for structured pass/fail limits), reference WorkflowEngine.Contracts. When you create a .NET or Mixed package through the TAT UI, WorkflowEngine.Contracts.dll IS automatically copied into assemblies/.

<Reference Include="WorkflowEngine.Contracts">
  <HintPath>..\..\assemblies\WorkflowEngine.Contracts.dll</HintPath>
</Reference>

Class and method requirements

  • The class MUST be static, or a regular class with a public parameterless constructor.
  • The method MUST be public.
  • Both static and instance methods are supported.
  • async Task<T> and async Task are fully supported.

Parameter passing

The runner matches YAML parameters: keys to C# parameter names using reflection. All values arrive as strings and are automatically converted to the declared type.

C# type Conversion
string No conversion
int int.Parse
double double.Parse
bool bool.Parse
long long.Parse
decimal decimal.Parse

Parameters not present in parameters: are resolved from step-level variables. Parameters with default values are optional in YAML.

Return types

Situation Recommended return type
Single measurement, limits in YAML Scalar (double, bool, string)
Multiple output variables, limits in YAML Dictionary<string, object>
Multiple measurements, limits in code List<MeasurementPointBase>
Setup / teardown, no measurement void / Task
Return type Effect
Any scalar Stored as output variable result — use {{result}} in YAML
Dictionary<string, object> Keys become output variables
void / Task Success, no outputs
MeasurementPointBase subclass Single measurement point recorded
IEnumerable<MeasurementPointBase> Multiple measurement points recorded
Task<T> Async equivalent

{{result}} IS the implicit variable name for scalar returns. Do NOT confuse {{result}} with {{value}} — the latter looks for a variable named "value".

Examples

using WorkflowEngine.Contracts.MeasurementPoints;
using static WorkflowEngine.Contracts.Types;

// Scalar return — use {{result}} in YAML
public static double MeasureVoltage(string rail)
    => ReadRail(rail);

// Output variables
public static Dictionary<string, object> MeasureVoltage(string channel)
    => new() { ["voltage"] = ReadChannel(channel) };

// No output (setup/teardown)
public static void PowerOn(string rail) => ApplyPower(rail);

// Single numeric measurement (limits in code)
public static NumericMeasurementPoint MeasureVoltage(string channel)
    => new NumericMeasurementPoint
    {
        Name               = "VBOARD",
        Value              = ReadChannel(channel),
        LowerLimit         = 4.75,
        UpperLimit         = 5.25,
        Units              = "V",
        ComparisonOperator = ComparisonOperatorType.GreaterThanOrEqual
    };

// Multiple measurements
public static List<MeasurementPointBase> PowerTest(double voltage, double current)
    =>
    [
        new NumericMeasurementPoint { Name = "VOLTAGE", Value = voltage,
            LowerLimit = 4.5, UpperLimit = 5.5, Units = "V",
            ComparisonOperator = ComparisonOperatorType.GreaterThanOrEqual },
        new NumericMeasurementPoint { Name = "CURRENT", Value = current,
            LowerLimit = 0.8, UpperLimit = 1.2, Units = "A",
            ComparisonOperator = ComparisonOperatorType.GreaterThanOrEqual },
    ];

// Async
public static async Task<Dictionary<string, object>> MeasureAsync(string channel)
{
    double voltage = await ReadChannelAsync(channel);
    return new() { ["voltage"] = voltage };
}

ComparisonOperator values

Value Meaning
Equal value == limit
NotEqual value != expected
GreaterThan value > LowerLimit
GreaterThanOrEqual value >= LowerLimit
LessThan value < UpperLimit
LessThanOrEqual value <= UpperLimit
Log Informational only — always PASS

For a range check (low_limit ≤ value ≤ high_limit) use the YAML measurement: block — the engine handles range comparison automatically.

Emitting Artifacts

A step attaches files (images, Plotly charts, CSV, logs) to the execution report by writing them into the artifact directory. There is no method to call: when the step returns, the runner enumerates that directory and the executor uploads what it finds, attributing each file to the step that produced it.

static readonly string ArtifactDir =
    Environment.GetEnvironmentVariable("ARTIFACT_DIR") ?? "/data/artifacts";

Read the variable with that fallback rather than hard-coding the path: the env var is what makes the assembly correct inside a station, the fallback is what keeps it runnable outside one. See reference/environment.md.

There is no EmitArtifact() and no EmitPlotly(). Revisions of this page before 2026-09-17 documented both. Neither has existed in any shipped runner - there is no such method on any type in WorkflowEngine.Contracts, so code calling one does not compile. Assert bug #13. Write files to ARTIFACT_DIR instead. (The Python runner does provide emit_artifact()/emit_plotly() from runner 1.2.0, Assert 2.3.0 - see sdk/python-sdk.md. The .NET runner does not, so a .NET artifact carries no description.)

The executor empties the directory before every step and collects only the files directly in it, not subdirectories.

The file name decides how the artifact is displayed

The reconcile path infers the content type from the extension alone, so the file name is the only thing that controls how the artifact appears in the report:

Name it Stored as Report shows
*.png, *.jpg, *.gif, *.svg image/* inline thumbnail, click for full size
*.plotly.json application/json interactive Plotly chart
*.csv, *.tsv text/csv, text/tab-separated-values rendered HTML table
*.html text/html opened as a page
*.txt, *.log, *.json, *.xml, *.pdf, *.zip the matching type download link
anything else application/octet-stream download link

*.plotly.json is a double extension and it is load-bearing. A figure written as chart.json is stored as application/json too, but fails the name test and renders as raw JSON rather than a chart.

Inline image example

using System;
using System.IO;

public static class BodeSteps
{
    static readonly string ArtifactDir =
        Environment.GetEnvironmentVariable("ARTIFACT_DIR") ?? "/data/artifacts";

    public static void PlotBode(string frequencies, string gains)
    {
        Directory.CreateDirectory(ArtifactDir);
        var path = Path.Combine(ArtifactDir, "bode-plot.png");

        SavePngTo(path);   // any charting library that can write a PNG
    }
}

The PNG is rendered inline as a thumbnail in the step row of the test report.

Interactive Plotly example

public static void BodeInteractive(string frequencies, string gains)
{
    Directory.CreateDirectory(ArtifactDir);

    // Any JSON matching the Plotly figure schema: { "data": [...], "layout": {...} }
    var figureJson = BuildPlotlyFigureJson(frequencies, gains);

    // .plotly.json, not .json - the double extension is what makes the UI render a chart.
    File.WriteAllText(Path.Combine(ArtifactDir, "bode-plot.plotly.json"), figureJson);
}

Working example in the shipped samples: sample-advanced-features (python_modules/bode_artifacts.py) writes a PNG, a Plotly chart and a CSV. The mechanism is the same directory for both runners, so it is a valid reference for .NET steps too.


Error signalling

Throw any exception → runner catches it → ABORTED verdict. The full exception message IS stored and displayed in the UI.

Returning null from a Dictionary<string, object> method DOES NOT fail the step — it is treated as an empty output set.

Logging from test code

Write to Console.Out or Console.Error. Output IS captured and displayed in the step log. Optional [LEVEL] prefixes are parsed:

Console.WriteLine("Connecting to " + address);        // Information
Console.WriteLine("[DEBUG] Raw response: " + raw);    // Debug
Console.Error.WriteLine("[WARNING] Retry 1 of 3");    // Warning

Supported prefixes: [INFO], [DEBUG], [WARNING] / [WARN], [ERROR] / [ERR], [TRACE].

An unhandled error has occurred. Reload

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.