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.

The documents can also be written at build time with dotnet getdocument, next to the OpenAPI and AsyncAPI ones; see Build-time document generation. The tool starts the app, so the same validation fails the build when a step references something that no longer exists.

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.

Analyzers

Bielu.AspNetCore.Arazzo ships the BARAZZO Roslyn analyzers, which check workflow-builder code at compile time. Most of these mistakes otherwise surface as an exception at startup, and two of them (BARAZZO006 and BARAZZO007) aren't caught at runtime at all.

Rule Severity Reports
BARAZZO001 Warning AddOpenApi() called after AddArazzo() in the same method, so build-time generation may miss the Arazzo documents (see Build-time Generation).
BARAZZO002 Error A literal workflow id, step id or source name, or a marker type's derived id, that doesn't match ^[A-Za-z0-9_-]+$.
BARAZZO003 Error A step that sets no target, or more than one, of Operation, OperationPath, Channel and Workflow.
BARAZZO004 Error A Channel step action other than "send" or "receive" (case-sensitive).
BARAZZO005 Warning WithCorrelationId on a step that doesn't receive: a send step, or an operation or workflow step.
BARAZZO006 Warning A Channel or OperationPath step naming a source description the document never declares.
BARAZZO007 Warning DependsOn or Workflow naming a workflow, or a step of the same workflow, that the document never defines.
BARAZZO008 Info AddAsyncApiSource/AddOpenApiSource pointing at a document name the project never registers with AddAsyncApi/AddOpenApi.

The analyzers only read what's written inline: string constants, marker types, and the lambdas passed to AddArazzo, AddWorkflow and Step. When a check can't see the whole picture, it stays quiet rather than guess. That happens when a builder is passed to a helper method, an id isn't a constant, a step's lambda branches, or something other than AddArazzo configures ArazzoOptions (for example Configure<ArazzoOptions>). Several AddArazzo calls with the same document name are merged, as they are at runtime.

⚠️ 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.