## Summary
### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.
### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.
### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.
## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.
Review and validation artifacts are maintained outside the repository.
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
11 KiB
Runtime Schema Generation
The Opc.Ua.Core.Schema library generates schemas for OPC UA data types at runtime. It works for the encodeable types emitted by the source generators and for complex types that are added dynamically by the complex-type client. Schemas are produced in every supported encoding:
- XSD for the XML encoding.
- BSD (OPC Binary, Part 6) for the binary encoding.
- JSON Schema (Part 6 Annex C, draft 2020-12) for the JSON encoding, in both the compact (reversible, BrowseName-keyed) and verbose flavors.
Schemas are built as strongly-typed object models in code — there are no embedded schema strings — so unused generation paths are trimmed away and the whole library is NativeAOT compatible. The XSD object model is the in-box System.Xml.Schema.XmlSchema. BSD uses
the existing Opc.Ua.Schema.Binary.TypeDictionary, and JSON uses
System.Text.Json.Nodes.JsonObject.
Contents
- Concepts
- Registration
- Registering data types
- Generating a schema
- Working with the object model
- JSON encoding notes
- PubSub schemas
- Trimming and NativeAOT
Concepts
Generation is driven by a type's runtime structure definition (StructureDefinition or EnumDefinition), which already captures every field, data type, value rank and optionality. The pieces fit together as follows:
ISchemaProvideris the entry point. It produces anIUaSchemafor a requestedUaSchemaFormatandUaSchemaScope.IUaSchemais the generated document. It exposes the strongly-typed object model (for exampleJsonSchemaDocument.Root,XmlSchemaDocument.Schema,BinarySchemaDocument.Dictionary) and can serialize itself withWriteTo(Stream),WriteTo(TextWriter)orToSchemaString().IDataTypeDefinitionResolvermaps a data type id to itsUaTypeDescription(the type id, browse name and definition). The default implementation,DataTypeDefinitionRegistry, is an in-memory registry that callers populate from browsed or otherwise available definitions. The resolver is also used to follow field references and to enumerate the types of a namespace.
UaSchemaFormat selects the encoding (Xsd, Bsd, JsonCompact, JsonVerbose). UaSchemaScope selects the document granularity: Type produces a document for a single type and the closure of the types it depends on; Namespace produces a dictionary document for all types in a namespace.
Registration
The services are registered through the standard OPC UA dependency-injection surface:
IServiceProvider services = new ServiceCollection()
.AddOpcUa()
.AddSchemaGeneration()
.Services
.BuildServiceProvider();
ISchemaProvider provider = services.GetRequiredService<ISchemaProvider>();
The provider can also be constructed directly when dependency injection is not used:
var registry = new DataTypeDefinitionRegistry();
registry.Add(new UaTypeDescription(typeId, browseName, structureDefinition, namespaceUri));
ISchemaProvider provider = new DefaultSchemaProvider(
registry,
[
new JsonSchemaGenerator(),
new XsdSchemaGenerator(),
new BsdSchemaGenerator()
]);
Registering data types
Schema generation needs the runtime definition of a type. A type's StructureDefinition / EnumDefinition is registered with the resolver from whichever source has it:
- Server / browsed types — a
DataTypeNodeobtained from a server (or the client node cache) carries its definition inDataTypeNode.DataTypeDefinition. Register it directly:
var registry = serviceProvider.GetRequiredService<DataTypeDefinitionRegistry>();
registry.TryAddDataType(dataTypeNode, session.NamespaceUris);
- Source-generated types — the generated model registration extension adds
public structure and enumeration activators to an
IEncodeableFactory. These activators implementIDataTypeDefinitionSource; the generated data value does not need to implement that interface. UseEncodeableFactoryDefinitionSourceto resolve their definitions without reflection or manual registration:
IEncodeableFactory factory = EncodeableFactory.Create();
factory.Builder
.AddOpcUa() // use the generated Add<Namespace>() extension for your model
.Commit();
IDataTypeDefinitionResolver resolver =
new EncodeableFactoryDefinitionSource(factory, namespaceUris);
ISchemaProvider provider = new DefaultSchemaProvider(
resolver,
[
new JsonSchemaGenerator(),
new XsdSchemaGenerator(),
new BsdSchemaGenerator()
]);
-
Dynamic complex types — complex types built by the complex-type client carry a
StructureDefinition(viaIStructureTypeInfo/ the structure-definition attribute) that can likewise be wrapped in aUaTypeDescriptionand registered. -
Types already in the encodeable factory — any structured or enumeration type known to an
IEncodeableFactory(source-generated types and runtime complex-type stand-ins) exposes its definition throughIDataTypeDefinitionSource, soEncodeableFactoryDefinitionSourceresolves it directly from the factory without a separate registration. Built-in primitive DataTypes (and simple types that derive directly from them) carry noDataTypeDefinitionand are handled by the built-in type system rather than this resolver. Use the factory source on its own, or combine it with a manualDataTypeDefinitionRegistry(for schema-only types that have no encodeable) viaCompositeDataTypeDefinitionResolver:
IDataTypeDefinitionResolver resolver = new CompositeDataTypeDefinitionResolver(
[new EncodeableFactoryDefinitionSource(factory, namespaceUris), registry]);
When AddComplexTypeSystem() is enabled, an OPC UA server wires this up
automatically (see ComplexTypes.md). The primed
encodeable factory becomes the schema resolver.
Once registered, fields that reference other registered types are resolved automatically and included in the generated document.
Namespace identity
BrowseName.NamespaceIndex is not a mapping for the data type's NodeId namespace; the
two namespaces may legitimately differ. To support both local and URI-based lookup in
DataTypeDefinitionRegistry, register an index-form TypeId together with its namespace
URI, as TryAddDataType(node, namespaceTable) does:
registry.Add(new UaTypeDescription(
new ExpandedNodeId(dataTypeNode.NodeId),
dataTypeNode.BrowseName,
definition,
session.NamespaceUris.GetString(dataTypeNode.NodeId.NamespaceIndex)));
A custom URI-only TypeId is resolvable by URI but does not create an inferred local alias. The standard OPC UA namespace URI is the exception because its local index is always zero. Registry entries must share one local namespace-table context.
URI lookup preserves identifier kind as well as value. Numeric zero, an empty GUID, an empty string, and an empty ByteString are distinct identifiers in a nonzero namespace; they must not be conflated with namespace-zero null NodeIds.
Generating a schema
Once a type's definition is registered with the resolver, a schema can be produced from its type id:
if (provider.TryGetSchema(typeId, UaSchemaFormat.JsonCompact, UaSchemaScope.Type, out IUaSchema? schema))
{
string json = schema.ToSchemaString();
}
When the starting point is a namespace-index NodeId obtained from a server, convert it
with the session namespace table. A registry populated with
TryAddDataType(dataTypeNode, session.NamespaceUris) accepts both this URI form and the
local index-form ExpandedNodeId:
ExpandedNodeId typeId = NodeId.ToExpandedNodeId(dataTypeNode.NodeId, session.NamespaceUris);
provider.TryGetSchema(typeId, UaSchemaFormat.JsonCompact, UaSchemaScope.Type, out IUaSchema? schema);
The convenience extension methods read more naturally and make a type "expose" its schema:
IUaSchema xsd = provider.GetXmlSchema(type);
IUaSchema bsd = provider.GetBinarySchema(type);
IUaSchema jsonCompact = provider.GetJsonSchema(type);
IUaSchema jsonVerbose = provider.GetJsonSchema(type, verbose: true);
// Resolve by type id and produce JSON in one call.
provider.TryGetJsonSchema(typeId, out IUaSchema? schema);
Working with the object model
Because the schema is an object model, callers can inspect or post-process it before serializing. For JSON:
var document = (JsonSchemaDocument)provider.GetJsonSchema(type);
JsonObject root = document.Root; // the draft 2020-12 schema
document.WriteTo(stream); // UTF-8, indented
JSON encoding notes (Part 6)
The JSON schemas follow the Part 6 JSON encoding faithfully, matching what the stack's JsonEncoder produces:
Int64andUInt64are encoded as JSON strings (to avoid precision loss), so they are typed asstring.Float/Doubleaccept the special string valuesInfinity,-InfinityandNaN, so they are typed as["number", "string"].ByteStringis a base64string;DateTimeis adate-timestring;Guidis auuidstring.- The standard structured built-ins (
NodeId,Variant,ExtensionObject,DataValue, ...) are described once per document in the$defssection and referenced. - Compact enums are integers (with the allowed values listed via
oneOf); verbose enums are theName_Valuestrings.
PubSub schemas
The Opc.Ua.PubSub.Schema library generates JSON Schemas for the PubSub JSON message formats. It is registered with services.AddOpcUa().AddPubSubSchema() and exposes IPubSubSchemaProvider:
CreateDataSetSchema(metaData, fieldContentMask, verbose)— the per-DataSet payload object, one property perFieldMetaData. The field value shape follows the same Part 6 JSON rules as the core library, andDataSetFieldContentMaskcontrols whether each field is the raw value or aDataValueobject (with the mask-selectedStatusCode/SourceTimestamp/ ... members).CreateDataSetMessageSchema(metaData, messageContentMask, fieldContentMask, verbose)— a single DataSetMessage whose header fields are gated byJsonDataSetMessageContentMaskand whosePayloadis the DataSet schema above.CreateNetworkMessageSchema(metaData, networkContentMask, messageContentMask, fieldContentMask, verbose)— theua-dataNetworkMessage envelope, gated byJsonNetworkMessageContentMask;Messagesis an array of DataSetMessage schemas, or a single object whenSingleDataSetMessageis set.CreateMetaDataMessageSchema(metaData, verbose)— theua-metadatamessage.
The provider reuses the core ISchemaProvider to resolve complex (structured/enum) field data types, embedding them into the document $defs section.
Trimming and NativeAOT
The library opts into IsAotCompatible and avoids reflection-based serialization. XSD is written with System.Xml.Schema.XmlSchema, BSD with a direct System.Xml.XmlWriter, and JSON with System.Text.Json.Nodes / Utf8JsonWriter. Schema generation is a configuration-time activity, not a hot path; documents are built lazily and can be cached by the caller. Because the generation logic lives in its own assembly, it is trimmed away entirely when an application does not generate schemas.