Edifact.Structure
1.0.0-preview.1
See the version list below for details.
dotnet add package Edifact.Structure --version 1.0.0-preview.1
NuGet\Install-Package Edifact.Structure -Version 1.0.0-preview.1
<PackageReference Include="Edifact.Structure" Version="1.0.0-preview.1" />
<PackageVersion Include="Edifact.Structure" Version="1.0.0-preview.1" />
<PackageReference Include="Edifact.Structure" />
paket add Edifact.Structure --version 1.0.0-preview.1
#r "nuget: Edifact.Structure, 1.0.0-preview.1"
#:package Edifact.Structure@1.0.0-preview.1
#addin nuget:?package=Edifact.Structure&version=1.0.0-preview.1&prerelease
#tool nuget:?package=Edifact.Structure&version=1.0.0-preview.1&prerelease
UN/EDIFACT
A .NET library that reads, validates, writes, projects and acknowledges UN/EDIFACT interchanges against the published UNECE directories — data-driven, with no generated code and no message types compiled into it.
Targets net8.0 and net10.0. Apache-2.0, with a carve-out for the vendored UN directories; see
NOTICE.
The property that matters
Reading an interchange and writing it back gives the same characters. Not "something a parser accepts" — the same characters, including the delimiters it arrived with, the release characters somebody used redundantly, the line ending, the trailing newline, and the fragment of a segment a truncated transmission ended in the middle of.
That is the correctness asset the whole design is built around, because it is the only test that fails when something is quietly dropped rather than misunderstood. A validator that cannot hand back the file it was given has no way to show it understood the file. It holds over 3000 generated interchanges, the committed samples, a message taken apart into a tree and reassembled with its orphans in the order they arrived, and 2000 generated invoices projected to JSON or XML and read back. It holds for a generated CONTRL too — an acknowledgement this library cannot itself read is not an acknowledgement.
You can run it against your own traffic:
dotnet run --project src/Edifact.Cli -- validate your-file.edi --round-trip
When it fails it names the first position where the output diverges, with both sides shown and the whitespace made visible.
What it does
dotnet run --project src/Edifact.Cli -- ingest packs/sources/UNTDID/D96A
dotnet run --project src/Edifact.Cli -- inspect untdid-d96a
dotnet run --project src/Edifact.Cli -- validate samples/invoic-d96a-broken.edi
dotnet run --project src/Edifact.Cli -- validate samples/invoic-d96a.edi --guide packs/overlays/prototype-orders-rules.json
dotnet run --project src/Edifact.Cli -- project samples/invoic-d96a.edi
Ingest a UNECE release directory into a committed pack. Both vendored releases ingest with nothing unaccounted for: D.96A gives 462 data elements, 161 composites, 127 segments, 207 code lists and 125 messages; D.01B gives 641, 295, 233, 268 and 208, interactive messages included. A drift test asserts each committed pack is byte-for-byte what a fresh ingest produces.
Validate an interchange: envelope, segment table, data element representation, code lists, character repertoire. Nothing on the read path throws — every defect is a finding with a position, a severity, a stable rule identifier and the authority that raised it. A
MessageTreealways exists, even for a message the directory does not define.Severity follows how much authority stands behind a rule. A code the directory does not publish is a warning, because a published list is advisory on its own — D.01B's own entry for 4053 lists two codes and then instructs the reader to use Incoterms it never enumerates. A code an agreement forbids is an error, because somebody signed it. Real EANCOM traffic leans on that distinction constantly, and
samples/holds three anonymized interchanges that do.Layer implementation guides over a release as versioned JSON, repeatably and in precedence order, so a report can say "'DP' is not permitted for 3035 by eancom-2002-invoic; the base untdid-d96a directory does permit it" rather than a bare rule identifier. Guides are documents, never C#.
Project a matched message as JSON or XML and read either back. Or project a message type and everything it names — 42 segments, 63 composites, 201 elements and 101 code lists for D.96A INVOIC — so a builder does one fetch instead of walking a graph five levels deep.
Acknowledge an interchange with a CONTRL, in syntax version 3 or version 4. Every finding already carries the 1-based segment, data element and component positions a CONTRL is written in.
How it is put together
A directory release and a syntax version are independent axes, and conflating them is the mistake
this library was written to avoid. packs/compiled/ holds releases; packs/syntax/ holds what
ISO 9735 contributes. A D.01B interchange written in version 3 syntax is legal and common, and it
says so in UNB data element 0002 — so the envelope is validated against the version the interchange
declares, not against the version its pack was built for.
| Project | What it holds |
|---|---|
Edifact.Core |
Tokenizer, writer, document model, diagnostics, character repertoires. No project references, enforced by a test |
Edifact.Schema |
Runtime model, pack format, linker, ISO 9735 syntax packs |
Edifact.Untdid |
Dialect detection, ingestion, reconciliation |
Edifact.Structure |
Segment-table compiler, thread-set matcher, bounded repair. About 4 µs per segment |
Edifact.Overlays |
Guide documents, the composer, the closed rule language, the provenance ledger |
Edifact.Projection |
A message as JSON or XML and back |
Edifact.Validation |
Envelope, element and structural checks |
Edifact.Acknowledgement |
CONTRL for syntax versions 3 and 4 |
Edifact.Cli |
ingest, inspect, validate, project |
Edifact.Api |
A JSON API over the validator, and the OpenAPI document clients are generated from |
The dependency graph is written out in tests/Edifact.Architecture.Tests/LayeringTests.cs and
asserted exactly, so widening it is a deliberate edit visible in a diff. Edifact.Core depends on
nothing, which is what lets a file nobody has a directory for still be read and shown to somebody.
Outside the solution's layered projects sit two more: web/edifact-web, a Nuxt application that is
the page somebody pastes an interchange into, and Edifact.AppHost, which runs it and the API
together. Neither is a layer — they are the delivery, not the library — and web/edifact-web/README.md
covers both.
Each layer's design notes live beside its code: packs/sources/README.md, packs/syntax/README.md,
src/Edifact.Overlays/README.md, src/Edifact.Projection/README.md,
src/Edifact.Validation/README.md. ROADMAP.md says what is done, what is not, and what was
deliberately left undone.
The web validator
dotnet run --project Edifact.AppHost
Starts the API and the Nuxt frontend together and opens the Aspire dashboard, which is where both
resources' URLs, logs and traces are. Paste an interchange into the page and it is read as you type:
the offending characters are underlined where they sit, each finding says which authority it
offended and which one that authority was layered over, and the message's structure is a tree you
can click into. web/edifact-web/README.md has the details, including running either half alone.
Running it
docker compose up --build
The validator on http://localhost:3000, built from this working copy. Or from what has been published, without a checkout at all:
TAG=0.1.0 docker compose up -d
Two images, ghcr.io/wimsuenens/edifact-api and ghcr.io/wimsuenens/edifact-web. Only the second
publishes a port: every request the browser makes reaches the API through the front end, which is
what keeps it same-origin and is why the API carries no CORS configuration to get wrong. GET /api/version says which build is answering, and is also the API container's readiness probe — the
server reads every pack before it serves anything, so a reply there means the packs parsed.
What each image reads from its environment:
Packs__Root |
Where the compiled packs are. Set in the image; the samples are found as a sibling of its parent. |
Build__Version, Build__Commit, Build__BuiltAt |
What /api/version reports. Set from build arguments. |
NUXT_API_URL |
Where the front end forwards /api/**. Read per request, so one image runs against any API. |
WEB_PORT |
What the front end is published on. Defaults to 3000. |
Releasing
Update CHANGELOG.md, then tag:
git tag v0.1.0 && git push --tags
The release workflow stands the built pair up and validates a known-bad interchange through it,
and separately installs the packed tool and uses it, before anything is published; then it pushes
the images, pushes the nine packages to nuget.org, and cuts the GitHub release from the changelog
section. Pushes to main publish edge and sha-<commit> images; nothing goes to nuget.org
except from a tag.
One version covers both, and it lives in Directory.Build.props rather than only in the tag — the
workflow refuses to publish a tag that disagrees with it. Its major is load-bearing and is not
free to move: the ingester records the assembly's major version in every pack manifest it writes,
and the committed packs are asserted byte-identical to a fresh ingest, so 1.0.0, 1.4.2 and
1.0.0-preview.1 all leave the packs alone while 0.x or 2.x would rewrite both for no change
to their contents. That is why the first published package is a 1.0.0 prerelease rather than a
0.1.0: the prerelease suffix, not the major, is what says the API is still moving.
Publishing needs a NUGET_USER repository variable naming the nuget.org account, and a Trusted
Publishing policy on nuget.org pointing at this repository and release.yml. There is no stored
API key — the workflow exchanges a GitHub OIDC token for one that lasts about an hour.
Building and testing
dotnet build Edifact.slnx
dotnet test Edifact.slnx
592 tests across 11 projects. The 542 covering the library run twice — once on net8.0, once on
net10.0 — so both runtimes must be installed; the older leg is not there for reach alone, it is
what stops a .NET 9-only convenience from being written in the first place. Nothing is conditionally
compiled. The 50 covering Edifact.Api run once, on net10.0: an application is not consumed by
reference, so nothing is served by holding it to the older runtime.
Counts in tests are exact, never "more than a hundred". A directory that came out plausible-looking and short by three records is the failure this library exists to make impossible, and a lower bound would not notice it.
The vendored directories
packs/sources/UNTDID/ is the UN/ECE distribution exactly as downloaded, byte for byte, and CI
checks all 31 files against the blobs recorded in the index on both Linux and Windows. It is CP437
fixed-column text in which the column of a box-drawing character carries segment-group nesting
depth, so .gitattributes marks the tree -text -diff.
The UN licence permits copying the Directory and passing it on unmodified; it does not permit
modifying and distributing it. That is why packs/compiled/ holds structure only — tags,
cardinalities, representations, code values — and no descriptive prose. Prose is read on demand from
the vendored files at runtime and is never committed in a transformed form.
Using it as a library
dotnet add package Edifact.Validation --prerelease
dotnet tool install --global Edifact.Cli --prerelease
Nine packages, net8.0 and net10.0, no external dependencies. Edifact.Validation is the usual
entry point and pulls in what it needs; Edifact.Core, .Schema, .Structure, .Overlays,
.Projection, .Untdid and .Acknowledgement are there for anyone who wants one layer of the
diagram above without the ones over it.
No directory data ships with them. A compiled pack is a derivative of the UN Directory, and a package would make every consumer a redistributor of it — a stronger claim than a public repository makes, and one this project has not made. So you supply the directory yourself, once:
edifact ingest ~/Downloads/D01B --packs ~/edifact/packs/compiled
export EDIFACT_PACKS=~/edifact/packs/compiled
Downloaded from UNECE, ingested on your machine, and nothing UN-derived crosses nuget.org. The ISO
9735 syntax definitions are in the tool, because they are authored here rather than derived from
anything (see packs/syntax/README.md) and no ingestion can produce them.
Status
Published as container images and as prerelease NuGet packages; see Running it and Using it
as a library. The packages are 1.0.0-preview.* rather than stable because the public API is
still moving — see ROADMAP.md for what is settled and what is not.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Edifact.Core (>= 1.0.0-preview.1)
- Edifact.Schema (>= 1.0.0-preview.1)
-
net8.0
- Edifact.Core (>= 1.0.0-preview.1)
- Edifact.Schema (>= 1.0.0-preview.1)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Edifact.Structure:
| Package | Downloads |
|---|---|
|
Edifact.Overlays
Trading partner and subset rules layered over a base directory — tightened cardinalities, restricted code values and conditional requirements — compiled into an effective message and evaluated with a ledger saying which overlay decided what. |
|
|
Edifact.Validation
Validates an interchange against a directory and any overlays: envelope structure, segment and element cardinality, representation and code values, each finding carrying the rule that produced it and where in the source it applies. |
|
|
Edifact.Projection
Projects a matched message into JSON or XML, either as the document it describes or as a linked form in which every value still points at the directory definition it was read against. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-preview.6 | 76 | 10/11/2026 |
| 1.0.0-preview.5 | 102 | 9/19/2026 |
| 1.0.0-preview.4 | 89 | 9/17/2026 |
| 1.0.0-preview.3 | 141 | 9/7/2026 |
| 1.0.0-preview.2 | 119 | 9/6/2026 |
| 1.0.0-preview.1 | 103 | 9/6/2026 |