Architecture
This page explains how the three packages fit together and the design decisions behind them. Useful if you want to extend the library, contribute, or build your own generator on top of the core.
Package layering
┌─────────────────────────────────┐ ┌──────────────────────────────────────┐
│ Invex.StructuredText │ │ │
│ .GithubActions │ │ Invex.StructuredText │
│ │ │ .AzureDevopsPipelines │
│ • GithubActionWriter │ │ │
│ • DependabotConfigWriter │ │ • DevopsPipelineWriter │
│ • GithubExpressionFormatter │ │ • DevopsExpressionFormatter │
│ • GithubActionModel/* │ │ • DevopsMacroExpression │
│ • DependabotConfigModel/* │ │ • DevopsRuntimeExpression │
│ │ │ • DevopsPipelineModel/* │
└───────────────┬─────────────────┘ └──────────────────┬───────────────────┘
│ │
└───────────────────┬───────────────────┘
▼
┌─────────────────────────────────────┐
│ Invex.StructuredText (core) │
│ │
│ • StructuredTextWriter │
│ • TextExpression hierarchy │
│ • TextExpressionFormatter (base) │
│ • TextExpressionCollection │
│ • WorkflowExpression<T> │
└─────────────────────────────────────┘
Core (Invex.StructuredText)
StructuredTextWriter
A minimal, indentation-aware text writer. WriteSection returns an IDisposable scope that decrements the indent on
dispose, so the structure of the generating C# code mirrors the structure of the generated YAML. A version counter
makes stale scopes harmless after Reset(). See StructuredTextWriter.
The expression tree
TextExpression is an abstract record with a closed-ish set of derived records grouped by concern:
- Values —
RawExpression,StringExpression,NumberExpression,BooleanExpression,NullExpression - Accessors —
PropertyAccessExpression,IndexAccessExpression,EvaluateExpression - Logic —
AndExpression,OrExpression,NotExpression,EqualExpression,NotEqualExpression, comparison expressions - Functions —
ContainsExpression,StartsWithExpression,EndsWithExpression,CoalesceExpression,FormatExpression,JoinExpression,ToJsonExpression,HashFilesExpression,ConcatExpression - Workflow-run —
StepOutputExpression,StepOutcomeExpression,TargetOutputExpression,TargetOutcomeExpression, and outcome-type literals - Typing helpers —
CastExpression<T>,WorkflowExpression<T>,WorkflowExpressionCollection<T>
Because they are records, expressions have structural equality and readable ToString() output, which makes tests and
debugging straightforward.
Fluent builder methods (Contains, EqualTo, And, …), operators (&, |, !, +), implicit conversions from
primitives, and an interpolated-string handler (TextExpressions.Format($"...")) make tree construction ergonomic.
TextExpressionFormatter
The abstract base class implements Format(TextExpression?) as a rewrite loop:
null→null;RawExpression→ its value;ConcatExpression→ concatenated formatted parts.CastExpression<T>is unwrapped transparently.- Otherwise the platform-specific
Resolve(expression)is called, which returns a simpler expression (often aRawExpressioncontaining rendered text). The loop repeats until everything bottoms out in raw text. - If
Resolvecannot handle a node, anInvalidOperationExceptionnames the offending expression.
This design means platform formatters only describe one rewrite step per node type and get recursion, casting, and null-handling for free.
Platform packages
Each platform package contains:
- Model types — records that mirror the official YAML schema for the platform. Almost every scalar property is a
TextExpression/WorkflowExpression<T>so any value can be an expression. - A writer — walks the model and emits YAML through a
StructuredTextWriter, calling the platform expression formatter for every expression-valued property. - An expression formatter —
GithubExpressionFormatterorDevopsExpressionFormatter, implementingResolvefor the platform's syntax.
Discriminated unions
YAML schemas are full of "one of" shapes (a step is run: or uses:; a pool is a name or a spec). These are
modeled as discriminated unions using the Dunet source generator: an abstract
base record with nested variant records. You construct the variant you need (new Step.RunStep { ... }), and the
writers pattern-match on the variant to emit the right YAML shape. This rules out invalid combinations at compile
time.
Schema fidelity
The GitHub workflow JSON schema and Dependabot v2 JSON schema are checked into the source tree alongside the models, and the Azure DevOps models carry XML docs lifted from the official YAML schema reference. Property names map mechanically to YAML keys (PascalCase → kebab-case / camelCase as appropriate per platform).
Public API discipline
The libraries use:
[PublicAPI]annotations (JetBrains.Annotations) on all public surface- Public API surface snapshot tests (
PublicApiSurfaceTestswith verified.txtbaselines in each test project), so any accidental API change shows up as a test diff - Multi-targeting for .NET 8 / 9 / 10
Extending the library
| Goal | How |
|---|---|
| New target platform | Reference the core, define model records, write a TextExpressionFormatter subclass and a writer over StructuredTextWriter |
| New expression node | Derive a record from TextExpression; handle it in each formatter's Resolve |
| Platform-specific expression syntax | Follow the pattern of DevopsMacroExpression / DevopsRuntimeExpression: define the record in the platform package and resolve it in that platform's formatter |