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.Arazzoonly 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.