Skip to content

6.7. PlantUML

Mikołaj Milewski edited this page Nov 12, 2026 · 1 revision

PlantUML

Stateflows.Extensions.PlantUml generates PlantUML text for registered State Machine and Activity definitions. The package adds inspection interceptors and handles PlantUmlInfoRequest events.

Register the extension with Stateflows:

services.AddStateflows(stateflows => stateflows
    .AddPlantUml()
    .AddStateMachines(machines => machines
        .AddStateMachine<Order>()));

Send a PlantUmlInfoRequest to a State Machine or Activity instance that can be inspected. Its response is a PlantUmlInfo containing the generated PlantUML string and convenience PNGUrl / SVGUrl links.

Those image links point to the public plantuml.com server and include the encoded diagram in the URL. Avoid sharing those links for diagrams containing sensitive information; render the returned PlantUML with a trusted local or privately hosted PlantUML server when needed. The extension source is available here.

Request a diagram

For a located IStateMachineBehavior or IActivityBehavior named behavior:

using Stateflows.Common;
using Stateflows.Extensions.PlantUml.Events;

var result = await behavior.RequestAsync<PlantUmlInfo>(new PlantUmlInfoRequest());
if (result.Status != EventStatus.Consumed || result.Response is null)
{
    throw new InvalidOperationException($"Diagram request failed: {result.Status}.");
}

await File.WriteAllTextAsync("behavior.puml", result.Response.PlantUml);

PlantUmlInfoRequest does not implicitly initialize a Behavior. Initialize the instance through its normal lifecycle before requesting a diagram when required. Reading PlantUml and writing it locally does not contact the public image server; do not fetch PNGUrl or SVGUrl for private models.

Structural interleaving annotations

Structured, parallel and iterative containers configured with .Interleave() (NodeOptions.Interleave) include <<interleave>> in their label. Parallel containers with a finite maxConcurrency additionally show the configured limit, for example:

partition "ParallelActivity jobs <<interleave>> [maxConcurrency=4]" {
}

These annotations describe execution settings, not additional Activity edges or an immediate boundary-output policy. maxConcurrency counts active Activity partitions per invocation, not CPU threads. Existing pin aliases, output aggregation, crossing-flow rules, token counts and status colors are unchanged. Custom inspection implementations that do not originate from the core model retain their existing labels.

Activity boundary pins

Activity diagrams include each AddInputPin<TToken> and AddOutputPin<TToken> boundary explicitly, labeled Input pin or Output pin with its declared token type. Unconnected pins are included, so the diagram also shows supported boundary types that currently have no connected flow.

.AddInputPin<Order>(pin => pin.AddFlow<ValidateOrder>())
.AddAction<ValidateOrder>(node => node.AddFlow<OrderResult, OutputPin<OrderResult>>())
.AddOutputPin<OrderResult>()

Pin flows connect to distinct diagram aliases rather than using shortened generic node names as identity. The same token type can therefore appear in root, nested, and sibling scopes without merging their pins. Nested pins remain inside the existing structured, parallel, or iterative partitions. Flow labels retain token types, weights, runtime token counts, and execution-status coloring.

The extension uses PlantUML's legacy Activity syntax: pins are labeled boundary activities, not native UML pin glyphs. Hidden self-links declare them independently of real data flows, including isolated pins; those links are not Activity edges and are not shown by the renderer.

INodeInspection.PinTokenType exposes the declared CLR token type to inspection consumers. It is null for nodes without a declared token type; custom inspection implementations inherit a null default for compatibility.

Clone this wiki locally