Strongly-typed data models
Why a typed model on top of a feature bag?
The per-spec dataset types (S101Dataset, S124Dataset, S421Dataset,
etc.) deliberately expose datasets as schema-agnostic feature bags:
flat collections of Feature and InformationType instances keyed
by string codes, with weakly-typed attribute dictionaries.
This shape is well-suited for the portrayal pipeline — the XSLT and Lua portrayal engines themselves walk attributes by name — but inconvenient for client code that wants to inspect domain concepts like routes, warnings, or aids to navigation directly.
The strongly-typed data model layer in
EncDotNet.S100.Core.DataModel provides the shared scaffolding for
spec-specific projections that turn a feature bag into a typed graph
organised around the spec's domain.
When to use which
| Use case | Layer |
|---|---|
| Portrayal pipeline, drawing instructions, generic feature iteration. | Feature-bag dataset (SxxxDataset, SxxxFeature). |
| Client code that wants typed access to spec-defined entities (routes, warnings, AtoN, etc.). | Typed data model (Sxxx{Root}.From(dataset, out diagnostics)). |
| Programmatic editing / serialising back to GML. | Not supported — typed models are read-only projections. |
Using a typed model
Open the dataset, then call the typed root's From method. It returns the
projection and a list of diagnostics:
using EncDotNet.S100.Datasets.S124;
using EncDotNet.S100.Datasets.S124.DataModel;
var dataset = S124Dataset.Open("navwarn_mixed.gml");
var warning = S124NavigationalWarning.From(dataset, out var diagnostics);
Console.WriteLine($"{warning.Preamble?.GeneralArea}, {warning.Preamble?.Locality}: {warning.Parts.Count} part(s)");
foreach (var diagnostic in diagnostics)
Console.WriteLine($"{diagnostic.Severity} {diagnostic.Code}: {diagnostic.Message}");
From throws only when the dataset is completely empty or lacks the root
entity. Everything else it can't read (an unresolved reference, an attribute
that doesn't parse, a feature without geometry) becomes a diagnostic, and the
projection keeps going. Always check the diagnostics: a projection with warnings
may be missing the entities they name. Each typed object keeps any source
attributes it didn't consume in ExtraAttributes.
Reading product data shows typed models alongside the other ways to read each product.
Typed roots by product
| Spec | Typed root | Notes |
|---|---|---|
| S-421 | S421RoutePlan |
Original precedent; refactored in Pass 1 to consume the shared abstractions. |
| S-124 | S124NavigationalWarning |
Pass 1 second consumer. |
| S-128 | S128ProductCatalogue |
Pass 2 — catalogue of nautical products, with resolved Supersedes / SupersededBy navigation. |
| S-125 | S125AtonDataset |
Pass 2 — marine aids to navigation. |
| S-201 | S201AtonInventory |
Pass 2 — IALA AtoN information. |
| S-122 | S122MarineProtectedAreaDataset |
Pass 2 — catalogue of MPAs / restricted areas / VTS areas with typed information-type bindings. |
| S-127 | S127MarineServicesDataset |
Pass 2 — marine resources and services. |
| S-129 | S129UnderKeelClearancePlan |
A single under-keel-clearance management plan. |
| S-131 | S131HarbourInfrastructureDataset |
Marine harbour infrastructure. |
| S-411 | S411SeaIceInventory |
An inventory of sea-ice and lake-ice features. |
Every GML-encoded product now has a typed root, each built with
Sxxx{Root}.From(dataset, out diagnostics). S-101 (ISO 8211) and the HDF5
coverage products (S-102, S-104, S-111) have none; read them through their
dataset types.
Shared abstractions
All in the EncDotNet.S100.Core package, namespace
EncDotNet.S100.DataModel:
ProjectionDiagnostic—Severity,Message,Code,RelatedId,RelatedAttribute. Stable codes such asxlink.unresolved,attribute.parse.int,feature.duplicate,feature.geometry.missing.DiagnosticSeverity—Info/Warning/Error.GeoPosition(double Latitude, double Longitude)— readonly record struct. WGS-84 / EPSG:4326 lat-lon ordering per S-100 Part 10b §6.2.ProjectionContext— bundle of diagnostics list + xlink resolver passed by reference through projection methods.AttributeParser—TryParseInt,TryParseDouble,TryParseBool,TryParseDateTimeOffset. Invariant culture; ISO 8601 round-trip per S-100 Part 5 §10. Failures emitattribute.parse.{type}diagnostics.XlinkResolver—gml:idlookup table. Strips the leading#fromxlink:href; misses emitxlink.unresolved; type mismatches also emit a diagnostic.ExtraAttributes.ExcludeKnown(...)— preserves any source attributes the typed model did not consume.
The GmlReference type (namespace EncDotNet.S100.Features, in the
EncDotNet.S100.Core package) is the shared
representation of an xlink:href cross-reference, replacing the
per-spec SxxxReference types from earlier iterations.
Contract for typed-model authors
When adding a typed model for a new product spec:
- Place the types in
src/EncDotNet.S100.Datasets.Sxxx/DataModel/under the namespaceEncDotNet.S100.Datasets.Sxxx.DataModel. - Provide a static factory
SxxxRoot.From(SxxxDataset, out IReadOnlyList<ProjectionDiagnostic>). - Never throw except for "fully empty dataset" / "missing root entity" cases. Treat everything else as a diagnostic.
- Build the xlink lookup via
XlinkResolver.Build(...)from the dataset's features and information types. Pass it into aProjectionContextthat you carry through projection methods. - Parse primitive attributes via
AttributeParser.TryParse*; never throw on parse failure. - Preserve unknown attributes via
ExtraAttributes.ExcludeKnown(attributes, ...known keys)so extensions and future-edition fields round-trip verbatim. - Reuse
GeoPositionfor coordinates andGmlReferencefor xlinks. - Keep typed-model projection independent of the portrayal pipeline: portrayal must continue to run from the feature-bag dataset without invoking the typed model.