Server-side complex types via the shared native-AOT path (#1306)#3955
Server-side complex types via the shared native-AOT path (#1306)#3955marcschier wants to merge 6 commits into
Conversation
…undation#1306) Enable an OPC UA server to build dynamic stand-in encodeables for the custom DataTypes it loaded from a NodeSet at runtime, reusing exactly the same NativeAOT-friendly path as the client (ComplexTypeSystem) instead of the legacy reflection-emit builder. - Move the shared complex-type machinery (ComplexTypeSystem, the resolver/ factory interfaces, DefaultComplexType* builder, DataDictionary) from Opc.Ua.Client to Opc.Ua.Core.Schema (namespace Opc.Ua.Schema) so both client and server use it. The client keeps NodeCacheResolver and the ComplexTypeSystem.Create(session, ...) helpers; the ISession constructors are replaced by those Create helpers. - Add a server-side AddressSpaceComplexTypeResolver over the in-memory address space, an opt-in fluent/DI AddComplexTypeSystem(), a ready-made ComplexTypeStandardServer, and IServerInternal.LoadComplexTypesAsync(). The pass runs after the address space is built and before endpoints open, and skips DataTypes already known to the factory (generated types). - Make the runtime stand-ins (Structure/Enumeration/OptionSet) implement IDataTypeDefinitionSource so the encodeable factory is the source of truth for data type definitions. The server exposes the primed factory as the schema IDataTypeDefinitionResolver via EncodeableFactoryDefinitionSource; the DataTypeDefinitionRegistry is kept only as an optional supplement for schema-only types (composed via CompositeDataTypeDefinitionResolver). - Add unit/integration tests and update Docs/ComplexTypes.md, Docs/SchemaGeneration.md and the 2.0 migration guide.
|
|
Addresses review feedback on OPCFoundation#3955: - Integrate runtime complex-type loading as the default in StandardServer via a new public LoadComplexTypes flag (opt out by setting it false), removing the ComplexTypeStandardServer subclass. The base OnNodeManagerStartedAsync now runs the pass and exposes the primed factory as the schema resolver. - OpcUaServerHostedService constructs a plain StandardServer and sets the complex-type options/registry/resolver-holder; AddComplexTypeSystem() now configures (and can opt out via ServerComplexTypeOptions.Enabled) rather than enabling the feature. - Docs: clarify that compiled/source-generated types are already registered in the IEncodeableFactory and used as-is, describe how and when generated types reach the factory, note built-in primitive types carry no DataTypeDefinition, and document the default-on behavior/opt-out.
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Enables OPC UA servers to build and register dynamic stand-in encodeables for runtime-loaded custom DataTypes by reusing the shared NativeAOT-friendly ComplexTypeSystem path (now shared in Opc.Ua.Core.Schema), and exposes the primed encodeable factory as the schema definition source.
Changes:
- Adds server-side complex type loading during
StandardServerstartup (pre-endpoint open) plus DI configuration viaAddComplexTypeSystem(). - Makes runtime stand-ins (
Structure/Enumeration/OptionSet) expose theirDataTypeDefinitionviaIDataTypeDefinitionSourceand updates factory-backed resolution/deduping. - Adds/updates unit tests across server and schema layers and migrates usages to the new shared
Opc.Ua.Schemanamespace.
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/Opc.Ua.Server.Tests/ServerComplexTypeSystemTests.cs | Adds server-side complex type loading tests (factory priming, skipping known, DI wiring). |
| Tests/Opc.Ua.Core.Schema.Tests/EncodeableFactoryDefinitionSourceTests.cs | Adds coverage for resolving DataTypeDefinition from runtime stand-ins via factory source. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/SchemaRegistrationTests.cs | Updates construction to new ComplexTypeSystem API after move to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolverTests.cs | Updates tests to use new ComplexTypeSystem constructor and schema namespace imports. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/MockResolver.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/Types/ComplexTypesCommon.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/TypeSystemClientTest.cs | Updates session-bound ComplexTypeSystem.Create(...) calls to new overloads. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/NodeCacheResolverTests.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/DefaultOptionSetTests.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/DefaultEnumerationTests.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/DefaultComplexTypesCommon.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/DefaultComplexTypesBuilderTests.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Client.ComplexTypes.Tests/DataDictionaryTests.cs | Updates imports to Opc.Ua.Schema. |
| Tests/Opc.Ua.Aot.Tests/ComplexTypeAotTests.cs | Switches to ComplexTypeSystem.Create(...) session helper usage. |
| Stack/Opc.Ua.Types/Encoders/Structure.cs | Implements IDataTypeDefinitionSource on runtime Structure stand-in. |
| Stack/Opc.Ua.Types/Encoders/Enumeration.cs | Implements IDataTypeDefinitionSource on runtime Enumeration stand-in. |
| Stack/Opc.Ua.Core.Types/Types/OptionSet.cs | Implements IDataTypeDefinitionSource on runtime OptionSet stand-in. |
| Stack/Opc.Ua.Core.Schema/Resolution/EncodeableFactoryDefinitionSource.cs | De-duplicates factory-known types when listing namespace types. |
| Stack/Opc.Ua.Core.Schema/Opc.Ua.Core.Schema.csproj | Adds InternalsVisibleTo for client and complex-types tests. |
| Libraries/Opc.Ua.Server/Server/StandardServer.cs | Adds async startup hook to load complex types before accepting connections. |
| Libraries/Opc.Ua.Server/Hosting/OpcUaServerHostedService.cs | Wires server complex type options/registry/resolver holder via DI. |
| Libraries/Opc.Ua.Server/Hosting/OpcUaServerComplexTypesBuilderExtensions.cs | Adds AddComplexTypeSystem() DI extension to configure/load resolver. |
| Libraries/Opc.Ua.Server/ComplexTypes/ServerDataTypeDefinitionResolver.cs | Adds mutable DI resolver holder for late-bound factory-backed resolver. |
| Libraries/Opc.Ua.Server/ComplexTypes/ServerComplexTypeSystem.cs | Adds IServerInternal.LoadComplexTypesAsync(...) implementation. |
| Libraries/Opc.Ua.Server/ComplexTypes/ServerComplexTypeOptions.cs | Introduces server load options (Enabled, OnlyEnumTypes, ThrowOnError). |
| Libraries/Opc.Ua.Server/ComplexTypes/AddressSpaceComplexTypeResolver.cs | Adds resolver over server in-memory address space for shared ComplexTypeSystem. |
| Libraries/Opc.Ua.Client/ComplexTypes/NodeCacheResolver.cs | Updates imports to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/IComplexTypeResolver.cs | Moves interface namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/IComplexTypeFactory.cs | Moves interface namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/DefaultComplexTypeFieldBuilder.cs | Moves implementation namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/DefaultComplexTypeFactory.cs | Moves implementation namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/DefaultComplexTypeBuilder.cs | Moves implementation namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/DataTypeException.cs | Moves exception namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/DataTypeDefinitionExtension.cs | Moves extension namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/DataDictionary.cs | Moves type namespace to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client/ComplexTypes/ComplexTypeSystem.cs | Moves ComplexTypeSystem to Opc.Ua.Schema and removes ISession ctors. |
| Libraries/Opc.Ua.Client.ComplexTypes/TypeBuilder/ComplexTypeFieldBuilder.cs | Updates imports to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client.ComplexTypes/TypeBuilder/ComplexTypeFactory.cs | Updates imports to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client.ComplexTypes/TypeBuilder/ComplexTypeBuilder.cs | Updates imports to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client.ComplexTypes/OpcUaComplexTypesBuilderExtensions.cs | Updates imports to Opc.Ua.Schema. |
| Libraries/Opc.Ua.Client.ComplexTypes/ComplexTypesExtensions.cs | Renames/reworks session-bound Create(...) helpers after shared move. |
| Libraries/Opc.Ua.Client.ComplexTypes/ComplexTypeSystemFactory.cs | Updates to build resolver from session node cache explicitly. |
| Docs/migrate/2.0.x/encoders.md | Updates migration guidance for schema assembly move + new constructors. |
| Docs/SchemaGeneration.md | Documents factory-backed IDataTypeDefinitionResolver usage via IDataTypeDefinitionSource. |
| Docs/ComplexTypes.md | Updates client guidance and adds server-side complex type loading section. |
| Applications/ConsoleReferenceClient/Program.cs | Updates sample to use ComplexTypeSystem.Create(session, telemetry). |
| Applications/ConsoleReferenceClient/ClientSamples.cs | Updates imports to Opc.Ua.Schema. |
Comments suppressed due to low confidence (2)
Tests/Opc.Ua.Server.Tests/ServerComplexTypeSystemTests.cs:1
[Parallelizable]on a fixture that uses shared mutable instance fields (e.g.,m_factory,m_nodesById, mocks set up in[SetUp]) can cause test interference because NUnit typically reuses one fixture instance across tests. To avoid race conditions/flaky tests, either remove[Parallelizable]here or add[FixtureLifeCycle(LifeCycle.InstancePerTestCase)](and keep per-test state isolated).
Stack/Opc.Ua.Core.Schema/Resolution/EncodeableFactoryDefinitionSource.cs:1- De-duplicating
GetNamespaceTypes(...)by(description.Name, description.NamespaceUri)can incorrectly drop distinct types that share a BrowseName within the same namespace (BrowseName uniqueness is not guaranteed). A safer de-dup key is the resolved type identifier (e.g.,description.TypeId/ the canonical data type id) rather than the qualified name.
Addresses review feedback on OPCFoundation#3955 (namespace thread): move the shared complex-type types (ComplexTypeSystem, IComplexType* interfaces, default builder/factory, DataDictionary, DataTypeDefinitionExtension, DataType*Exception) from Opc.Ua.Schema to the root Opc.Ua namespace. Opc.Ua is already imported by consumers, so existing code keeps compiling without adding a new using; the pre-existing schema resolution/generation types stay in Opc.Ua.Schema. - Renamespaced the 9 moved files to Opc.Ua; ComplexTypeSystem/DataDictionary add 'using Opc.Ua.Schema;' for the remaining registry/validator types. - Removed the now-unused 'using Opc.Ua.Schema;' from complex-type consumers (client, client.complextypes, server resolver, samples, tests); kept it where remaining Opc.Ua.Schema types are still used. - Updated ComplexTypes.md and migrate/2.0.x/encoders.md namespace references and migration guidance. Verified: Core.Schema/Client/Client.ComplexTypes/Server + ConsoleReferenceClient build clean; 2729 Client.ComplexTypes.Tests and server complex-type/schema tests pass.
…ror context (PR feedback) Addresses 3 review comments on OPCFoundation#3955: - AddressSpaceComplexTypeResolver.GetEnumTypeArrayAsync no longer returns the first non-null HasProperty target (which could be an unrelated property). It now matches the well-known EnumValues/EnumStrings/OptionSetValues by BrowseName and selects them in an explicit priority order. - LoadDataTypesAsync throws a ServiceResultException with a meaningful OPC UA StatusCode (BadNodeIdUnknown when missing, BadNodeClassInvalid otherwise) and includes the node id in the message, instead of a generic message. - AddComplexTypeSystem() registers IDataTypeDefinitionResolver via TryAddSingleton (matching AddSchemaGeneration), keeping registration idempotent across multiple/transitive calls. - Added ServerComplexTypeSystemTests covering the property-selection fix (ignores an unrelated property; prefers EnumValues over EnumStrings).
…s branch Improves changed-line coverage for the server complex-type feature and removes dead code: - AddressSpaceComplexTypeResolver.LoadDataTypesAsync: FindAsync only ever returns a DataTypeNode or null, so the BadNodeClassInvalid branch was unreachable. Simplified to a single BadNodeIdUnknown throw with a clearer message. - Added ServerComplexTypeSystemTests covering the previously untested resolver methods: LoadDataTypeSystem, BrowseTypeIdsForDictionaryComponentAsync, FindSuperTypeAsync, FindAsync (DataType and non-DataType), both BrowseForEncodingsAsync overloads, and LoadDataTypesAsync (custom subtypes, add-root-node, and the not-a-DataType throw path). All 15 ServerComplexTypeSystemTests pass; Opc.Ua.Server builds clean.
Description
Enable an OPC UA server to build dynamic stand-in encodeables for the custom DataTypes it loaded from a NodeSet at runtime, so a
Variant/ExtensionObjectcarrying a custom type "just works" without a compiled .NET type. The server reuses exactly the same NativeAOT-friendly path as the client (ComplexTypeSystem) rather than a parallel implementation, and does not touch the legacy reflection-emit builder.What changed
Shared machinery moved to a common assembly
ComplexTypeSystem, the resolver/factory interfaces, theDefaultComplexType*builder andDataDictionaryfromOpc.Ua.ClienttoOpc.Ua.Core.Schema(namespaceOpc.Ua.Schema) so both client and server use it (git history preserved via renames).NodeCacheResolverand gainsComplexTypeSystem.Create(session, …)helpers; theISessionconstructors are replaced by those helpers (the reflection-emit builder is opt-in viaCreate(session, new ComplexTypeBuilderFactory(), telemetry)).Server-side complex types
AddressSpaceComplexTypeResolverreads the server's in-memory address space (DataType nodes +DataTypeDefinition).AddComplexTypeSystem(), a ready-madeComplexTypeStandardServer, andIServerInternal.LoadComplexTypesAsync(…). A new asyncStandardServer.OnNodeManagerStartedAsynchook runs the pass after the address space is built and before endpoints open. DataTypes already backed by a compiled/generated type are skipped; only runtime-loaded ones become stand-ins.Encodeable factory as the source of truth for data type definitions
Structure/Enumeration/OptionSet) now implementIDataTypeDefinitionSource, soEncodeableFactoryDefinitionSourceresolves generated and runtime types. The server exposes the primed factory as the schemaIDataTypeDefinitionResolver;DataTypeDefinitionRegistryis kept only as an optional supplement for schema-only types (composed viaCompositeDataTypeDefinitionResolver). No address-space walk or separate registry population is required.Tests
ServerComplexTypeSystemTests(factory priming by type + encoding id, skip-known, factory-backed resolver + composite fallthrough, DI registration) and new stand-in resolution tests inEncodeableFactoryDefinitionSourceTests.Opc.Ua.Core.Schema.Tests(67), the server complex-type tests, and 2228 client complex-type tests pass; net48 + net10 builds are 0‑warning.Docs
Docs/ComplexTypes.md(new server section),Docs/SchemaGeneration.md(factory-as-resolver), andDocs/migrate/2.0.x/encoders.md.Related Issues
Checklist
Put an
xin the boxes that apply. You can complete these step by step after opening the PR.