mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-28 22:55:49 +08:00
# Description Fix runtime schema resolution and provide a supported DI-free construction path for generated data types. - Resolve registered data types by local/index-form and namespace-URI identifiers without using BrowseName as a namespace mapping. - Preserve identifier kind in URI registry keys, so numeric zero and empty GUID/string/ByteString identifiers remain distinct in nonzero namespaces, including during replacement. - Reconstruct typed identifiers during factory URI-to-index fallback while retaining exact lookup precedence and unknown-namespace/remote-server guards. - Make the built-in JSON, XSD, and BSD generators public, stabilize `EncodeableFactoryDefinitionSource`, and document generated activator/factory usage. - Keep the stable-API assertion effective across the netstandard2.1/net8.0 polyfill boundary by inspecting attribute metadata without referencing the ambiguous CLR type. - Merge the latest `master` without dropping upstream changes. ## Related Issues - Fixes #4423 - Fixes #4424 ## Validation - `Opc.Ua.Core.Schema.Tests`: 157/157 on `net10.0` - `Opc.Ua.Core.Schema.Tests`: 157/157 on `net48` - `Opc.Ua.Core.Schema.Tests`: 157/157 with `CustomTestTarget=netstandard2.1` and its `net8.0` consumer - Managed `SchemaAotTests`: 4/4 - NativeAOT Release publish and filtered `SchemaAotTests`: 4/4 - All three review reproductions pass against rebuilt assemblies. ## Checklist - [ ] 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. - [x] 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. - [x] 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: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b17fd166-fdef-4660-a9d5-97c0e69f1bd5
188 lines
11 KiB
Markdown
188 lines
11 KiB
Markdown
# 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`, the BSD object model is the existing `Opc.Ua.Schema.Binary.TypeDictionary`, and the JSON object model is `System.Text.Json.Nodes.JsonObject`.
|
|
|
|
## 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:
|
|
|
|
- `ISchemaProvider` is the entry point. It produces an `IUaSchema` for a requested `UaSchemaFormat` and `UaSchemaScope`.
|
|
- `IUaSchema` is the generated document. It exposes the strongly-typed object model (for example `JsonSchemaDocument.Root`, `XmlSchemaDocument.Schema`, `BinarySchemaDocument.Dictionary`) and can serialize itself with `WriteTo(Stream)`, `WriteTo(TextWriter)` or `ToSchemaString()`.
|
|
- `IDataTypeDefinitionResolver` maps a data type id to its `UaTypeDescription` (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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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 `DataTypeNode` obtained from a server (or the client node cache) carries its definition in `DataTypeNode.DataTypeDefinition`. Register it directly:
|
|
|
|
```csharp
|
|
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`. Those activators
|
|
implement `IDataTypeDefinitionSource`; the generated data value itself does not need
|
|
to implement that interface. Use `EncodeableFactoryDefinitionSource` to resolve their
|
|
definitions without reflection or manual registration:
|
|
|
|
```csharp
|
|
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` (via `IStructureTypeInfo` / the structure-definition attribute) that can likewise be wrapped in a `UaTypeDescription` and 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 through `IDataTypeDefinitionSource`, so `EncodeableFactoryDefinitionSource` resolves it directly from the factory without a separate registration. Built-in primitive DataTypes (and simple types that derive directly from them) carry no `DataTypeDefinition` and are handled by the built-in type system rather than this resolver. Use the factory source on its own, or combine it with a manual `DataTypeDefinitionRegistry` (for schema-only types that have no encodeable) via `CompositeDataTypeDefinitionResolver`:
|
|
|
|
```csharp
|
|
IDataTypeDefinitionResolver resolver = new CompositeDataTypeDefinitionResolver(
|
|
[new EncodeableFactoryDefinitionSource(factory, namespaceUris), registry]);
|
|
```
|
|
|
|
An OPC UA server wires this up automatically when `AddComplexTypeSystem()` is enabled (see [ComplexTypes.md](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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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`:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
- `Int64` and `UInt64` are encoded as JSON strings (to avoid precision loss), so they are typed as `string`.
|
|
- `Float`/`Double` accept the special string values `Infinity`, `-Infinity` and `NaN`, so they are typed as `["number", "string"]`.
|
|
- `ByteString` is a base64 `string`; `DateTime` is a `date-time` string; `Guid` is a `uuid` string.
|
|
- The standard structured built-ins (`NodeId`, `Variant`, `ExtensionObject`, `DataValue`, ...) are described once per document in the `$defs` section and referenced.
|
|
- Compact enums are integers (with the allowed values listed via `oneOf`); verbose enums are the `Name_Value` strings.
|
|
|
|
## 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 per `FieldMetaData`. The field value shape follows the same Part 6 JSON rules as the core library, and `DataSetFieldContentMask` controls whether each field is the raw value or a `DataValue` object (with the mask-selected `StatusCode` / `SourceTimestamp` / ... members).
|
|
- `CreateDataSetMessageSchema(metaData, messageContentMask, fieldContentMask, verbose)` — a single DataSetMessage whose header fields are gated by `JsonDataSetMessageContentMask` and whose `Payload` is the DataSet schema above.
|
|
- `CreateNetworkMessageSchema(metaData, networkContentMask, messageContentMask, fieldContentMask, verbose)` — the `ua-data` NetworkMessage envelope, gated by `JsonNetworkMessageContentMask`; `Messages` is an array of DataSetMessage schemas, or a single object when `SingleDataSetMessage` is set.
|
|
- `CreateMetaDataMessageSchema(metaData, verbose)` — the `ua-metadata` message.
|
|
|
|
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.
|