Table of Contents

Arazzo overview

Bielu.Arazzo.NET and Bielu.Arazzo.NET.Readers provide a framework-free object model, writers, reader, and validator for the Arazzo Specification — a standard for describing multi-step API workflows. Arazzo 1.1 added asyncapi as a first-class sourceDescriptions type alongside openapi, so a single workflow can span HTTP operations and event/message channels.

⚠️ Note: Before version 1.0.0, these libraries — and Bielu.AspNetCore.Arazzo, the ASP.NET Core integration covered below — are regarded as unstable and breaking changes may be introduced.

Installation

dotnet add package Bielu.Arazzo.NET
dotnet add package Bielu.Arazzo.NET.Readers

Bielu.Arazzo.NET has no dependency on YAML or on anything else in this repo — consumers that only build and write documents (source generators, analyzers) never pull in a YAML parser. YamlDotNet is confined to Bielu.Arazzo.NET.Readers.

Reading a document

using Bielu.Arazzo.Readers;

var result = ArazzoStringReader.Read(yamlOrJsonText);

if (result.Document is null)
{
    foreach (var error in result.Diagnostics.Errors)
    {
        Console.WriteLine($"{error.Path}: {error.Message}");
    }
    return;
}

var document = result.Document;

ArazzoStringReader auto-detects JSON vs. YAML from the input's first non-whitespace character. ArazzoStreamReader and ArazzoTextReader cover the Stream/TextReader equivalents. Reading never throws for malformed input — problems are reported as diagnostics on the returned ArazzoReadResult.

Validating a document

using Bielu.Arazzo.Validation;

var errors = ArazzoValidator.Validate(document);

ArazzoValidator checks the structural invariants a well-formed document must satisfy beyond what the type system already enforces — unique workflowId/stepId values, step target mutual-exclusivity, JSON Schema shape checks on workflow.inputs, and more. It does not resolve references against real source documents (does this operationId actually exist?) — that is ArazzoWorkspace's job.

Writing a document

using Bielu.Arazzo.Writers;

var json = ArazzoJsonWriter.Write(document);
var yaml = ArazzoYamlWriter.Write(document);

Both writers serialize through the same IArazzoWriter tree-builder abstraction (ArazzoJsonNodeWriter), and each model type implements IArazzoSerializable.SerializeAsV1 — a version-scoped serialization method, following the same pattern ByteBard's AsyncAPI.NET uses for its own model types.

Runtime expressions

The Bielu.Arazzo.Expressions namespace implements the full §5.9 runtime-expression grammar — $url, $method, $statusCode, $request.*, $response.*, $message.*, $inputs.*, $outputs.*, $steps.*, $workflows.*, $sourceDescriptions.*, $components.*, and $self:

using Bielu.Arazzo.Expressions;

if (RuntimeExpressionParser.TryParse("$message.payload#/status", out var expression, out var error))
{
    // expression is a RuntimeExpression.Message with a JSON Pointer into the payload
}

Resolving workflow steps against live documents

ArazzoWorkspace is the seam that lets a step's operationId/operationPath/channelPath reference be resolved against the actual OpenAPI/AsyncAPI/Arazzo documents a sourceDescription points at, rather than only checked for well-formedness:

using Bielu.Arazzo;

var workspace = new ArazzoWorkspace();
workspace.RegisterResolver(new MyAsyncApiSourceResolver());
workspace.RegisterDocument("events", "asyncapi", myAsyncApiDocument);

if (workspace.TryResolveOperation("events", "sendLightMeasurement", out var operation))
{
    // operation resolved against the live AsyncAPI document
}

Implement IArazzoSourceResolver per source type (openapi, asyncapi, arazzo). This is the hook Bielu.AspNetCore.Arazzo plugs into so a running app can self-wire its own IAsyncApiDocumentProvider and OpenAPI documents — turning a renamed channel or operation into a startup failure instead of a production one.

ASP.NET Core integration: Bielu.AspNetCore.Arazzo

Bielu.AspNetCore.Arazzo mirrors the core AsyncAPI package's shape: a fluent options builder, AddArazzo/MapArazzo, and — the differentiating feature — self-wiring sourceDescriptions against the same app's live AsyncAPI/OpenAPI documents.

dotnet add package Bielu.AspNetCore.Arazzo
using Bielu.Arazzo.Models;
using Bielu.AspNetCore.Arazzo.Extensions;

builder.Services.AddOpenApi("v1");
builder.Services.AddAsyncApi("v1");

builder.Services.AddArazzo(options =>
{
    options.WithInfo("Streetlights workflows", "1.0.0");
    options.AddAsyncApiSource("events", "v1");   // self-wires against the app's own AsyncAPI document
    options.AddOpenApiSource("api", "v1");       // self-wires against the app's own OpenAPI document

    options.AddWorkflow("measureAndAlert", wf => wf
        .Step("publishMeasurement", s => s
            .Channel("events", "lightMeasured", ArazzoStepAction.Send)
            .Output("measurementId", "$message.payload#/id"))
        .Step("awaitAlert", s => s
            .DependsOn("publishMeasurement")
            .Channel("events", "lightingAlert", ArazzoStepAction.Receive)
            .SuccessCriteria("$message.payload#/measurementId == $steps.publishMeasurement.outputs.measurementId")));
});

var app = builder.Build();
app.MapAsyncApi();
app.MapOpenApi();
app.MapArazzo();   // → /arazzo/{documentName}.json (default route; JSON only)
app.Run();

By default, MapArazzo() serves only JSON at /arazzo/{documentName}.json. To also serve YAML, map a second route with a .yaml/.yml pattern:

app.MapArazzo("/arazzo/{documentName}.yaml");

By default (ArazzoOptions.ValidateSourceReferencesOnStartup = true), every step's operationId/operationPath/channelPath is resolved against the live, in-memory AsyncAPI/OpenAPI documents once at app startup — a renamed channel or operation throws ArazzoStartupValidationException and fails startup, rather than failing the first time a workflow actually runs in production.

Identifying workflows and steps by type

Workflow and step ids are cross-referenced by string (dependsOn, a step targeting another workflow), which makes a typo a runtime problem rather than a compile-time one. Every id-taking builder method has a generic overload that takes a marker type instead, so renaming the type moves every reference with it:

// Marker types — they carry no members; the type itself is the identifier.
sealed class MeasureAndAlert;
sealed class PublishMeasurement;
sealed class AwaitAlert;

options.AddWorkflow<MeasureAndAlert>(wf => wf
    .Step<PublishMeasurement>(s => s
        .Channel("events", "lightMeasured", ArazzoStepAction.Send)
        .Output("measurementId", "$message.payload#/id"))
    .Step<AwaitAlert>(s => s
        .DependsOn<PublishMeasurement>()
        .Channel("events", "lightingAlert", ArazzoStepAction.Receive)));

options.AddWorkflow<ReportDaily>(wf => wf
    .DependsOn<MeasureAndAlert>()                       // workflow-level dependsOn
    .Step<Summarise>(s => s.Workflow<MeasureAndAlert>()) // a step targeting another workflow
);

The mapping is ArazzoId.FromType<T>(): the type name camel-cased, so MeasureAndAlert becomes measureAndAlert (and HTTPHealthCheck becomes httpHealthCheck). That keeps the emitted document's casing idiomatic while the marker types stay idiomatic C#, and it means the two forms interoperate — AddWorkflow("measureAndAlert", …) and DependsOn<MeasureAndAlert>() refer to the same workflow, so you can adopt the generic form incrementally.

⚠️ Security note: Bielu.AspNetCore.Arazzo only serves and validates workflow documents — it does not execute them. Any future execution engine is intended to be CLI/test-only by design, and would never be exposed as a default ASP.NET Core endpoint: a hosted endpoint that ran arbitrary workflow steps would let a document drive outbound requests from inside your application.

CLI Tool: dotnet arazzo

Bielu.Arazzo.Cli provides validate, lint, and diff commands for Arazzo documents — see the CLI Tool article for details.

What's next

A workflow runtime — executing steps, evaluating criteria, and propagating outputs — is the natural next piece, exposed through the CLI as a test/automation runner rather than as a hosted endpoint.