# Description Remove stock phrasing and correct misleading or inconsistent technical documentation. The initial two passes covered 367 Markdown files, followed by an interactive review of 169 editorial flags. The U5 interpretation originally deferred in that review has since received the upstream Mantis answer recorded by #4843; remaining server/spec follow-ups stay explicit in the CTT log. - Correct package responsibilities, migration steps, API names, ownership, protocol descriptions and links. Keep bundled migration guides synchronized. Preserve historical conclusions as historical records with separate re-check notes. - Provide at least two rendered TOC levels throughout `docs/`. Convert appropriate architecture figures to Mermaid without changing code examples, transcripts or historical quotations. - Address the October 19 feedback with concrete setup/migration instructions, corrected certificate paths and PubSub names, shorter repeated caveats, consistent XML comments, and a full review of the Concepts guide. Retain #4839's API corrections. - Integrate #4841's runtime/performance guidance and review incoming/recent prose. Preserve dated measurements rather than claiming new benchmark runs; clarify allocation ratios, startup-only processor settings, JIT warm-up and the benchmark commit's scope. Missing raw measurement artifacts and repeat ranges remain tracked by #4842. - Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log conflict using the updated Mantis states and OPC answers, retaining historical framing and re-checks for C28/C32/C43. - Address the latest naming/index feedback in `adb5bf693`: rename the guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the provider and DataType-declaration contracts into the conversion guide and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and `samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di, FluentApi, ISA95, Lds and PubSub; update solution entries, project references, Docker/CI paths, test fixture paths and documentation links with exact casing. **WoT guide scope.** This branch retains the detailed conversion guide that #4758 had replaced with an anchor stub. Conversion limits, verified document sets, preservation rules and provider ownership now live together in `WotNodeSetConversion.md`; binding/runtime guidance remains in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger prose was shortened, the oversized argument-table cell moved into its section, and existing anchors preserved. **SDK policy is a separate, explicit user-directed change.** `global.json` sets a stable minimum of `10.0.400`, `rollForward: latestMajor`, and `allowPrerelease: false`. Newer major SDKs are permitted; CI's explicit SDK pins are unchanged. README, GettingStarted, DeveloperGuide and Recycling instructions reflect the possible analyzer differences. The image-contract regression fix accepts compatible stable SDK images instead of requiring exact equality with the floor. The latest four rename/index threads are resolved after publication. Three older threads remain open: universal obsolete-removal timing, unconditional nonce-strength wording, and a blanket Vision NativeAOT claim. Existing implementation/evidence constraints are explained in their inline replies. The two-level TOCs remain as requested. ## Validation - `git diff --check` and staged diff checks: passed. The untouched upstream AutoID NodeSet retains its provenance bytes, including two existing license-header trailing spaces. - `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled migration documents and manifests synchronized. - Documentation/path validation: **101** documents retain two rendered TOC levels (**2,228** entries), with idempotent formatting. **52** new relative links and their anchors passed exact-case checks. No tracked references to the old guide/sample paths remain. Both relocated provider-contract sections are preserved in the destination. - `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects and the existing FluentApi/Lds/PubSub projects. Updated solution and pump ProjectReference paths also passed exact-case existence checks. - Latest focused commands, run from the repository root: ```powershell # Passed: 61 channel send-order and reconnect tests. dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal # Passed: 5 pump datasheet tests; builds the renamed pump sample. dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal # Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models. dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal # Passed: build, zero warnings/errors. dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal # Passed: dependent sample-test project build, zero warnings/errors. dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal ``` Earlier focused integration checks passed 2,620 tests across AutoID, event queues/scheduling, source generation, MCP/package profiles and WoT. Both reviewed Mermaid diagrams rendered locally without network access. These checks are not a full solution run. The full solution has **not** been rerun for this review. Earlier `dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did not pass: three AOT projects rejected VSTest and 20 Vision tests failed during graphics initialization with `OutOfMemoryException`. Remaining long-running tests were stopped at the user's request. A focused Vision rerun passed 558 tests and reproduced those 20 failures. The earlier `dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was blocked by the former SDK pin and was not retried after the policy change. The user approved proceeding with those limitations documented; current remote CI is not claimed green. ## Related Issues - Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and diagnostic-tool documentation refresh. #4841 supplies documentation and earlier results, not all newly requested measurements. - #4758 explains the conversion guide's earlier consolidation; the current provider contracts are now consolidated into `WotNodeSetConversion.md`. - #4839 supplies the reviewed API corrections; #4843 supplies the updated CTT issue record. - The original prose cleanup and SDK policy were user-requested without a source issue. No new runtime architecture is introduced by these documentation edits. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Roman Ettlinger <romanett98@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
14 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 (draft 2020-12) for the Part 6 JSON encoding, in both the compact 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 (Part 6)
- PubSub schemas
- OpenAPI document of the REST binding
- 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.
Standard field encodings
Number (i=26), Integer (i=27) and UInteger (i=28) are abstract simple
types, not structures requiring a DataTypeDefinition. When used as structure
fields, they use Variant encoding as specified by
OPC 10000-6, 5.1.6.
Their scalar and array field schemas therefore reference Variant rather than an
unconstrained value or an ExtensionObject.
SchemaTypeInfo.GetFieldEncodingType exposes this classification to callers that
walk schema dependencies. It also preserves the standard primitive/alias mappings;
custom-namespace identifiers such as ns=1;i=26 still require their own definitions.
It does not change the general-purpose TypeInfo classification.
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. - Variant fields, including abstract numeric structure fields, use the
UaTypeandValueenvelope with optionalDimensions. Null array entries are allowed, and numeric defaults such as zero retain an explicitValuein both Compact and Verbose encoding.Valueis omitted only for the NULL value of a nullable type (Part 6, 5.4.2.17, Table 41), while retainingUaType. The deprecatedType/Bodyenvelope is not the Compact/Verbose format. - 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.
OpenAPI document of the REST binding
WebApiOpenApiGenerator generates the OpenAPI 3.0 document of the REST binding
(Part 6 G.3, see REST binding) from WebApiServiceRoutes and the
structure definitions of the request and response types. It is not an
IUaSchemaGenerator: a document describes a set of routes rather than one data
type.
var generator = new WebApiOpenApiGenerator();
JsonObject document = generator.Generate(
WebApiServiceSet.Sessionless, // or AllServices
includeSchemas: true, // false: a JSON object as every body
serverUrl: "/opcua/"); // null: no servers list
Without includeSchemas the document holds the paths and operations only
(about 12 KB for all 28 services). With it, every request, response and the
structures and enumerations they contain become component schemas (about 46 KB),
as required by OpenAPI client generators. The schemas describe the Compact
JSON encoding with the property names and types the published OPC Foundation
documents use; OPC UA over OpenAPI lists the differences and the
tests that keep the document correct. Pass an IDataTypeDefinitionResolver to
the constructor to resolve the types from another source than the generated
types of the OPC UA namespace. The generator is registered by
AddWebApiTransport(), which serves the document.
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.