Seal [DataType] config classes and migrate remaining [DataContract] configs - #783
Merged
Merged
Conversation
…onfigs AreaConfiguration was a non-sealed partial class, so the source generator's default StructureHandling.Auto resolved to ExtensionObject wrapping for both AlarmConditionServerConfiguration.Areas and AreaConfiguration.SubAreas. The shipped AlarmConditionServer.Config.xml stores plain nested <AreaConfiguration> elements with no ExtensionObject/TypeId wrappers, so the area tree silently decoded as empty (0 of 8 areas, no exception). Auto resolves to inline encoding only when the field's type is sealed and has no IEncodeable base; otherwise it wraps in an ExtensionObject to allow subtyping. Sealing the config types is the fix, and none of them is subclassed anywhere in the repo. - Seal the 10 existing [DataType] types. AreaConfiguration, MemoryBufferInstance and PerformanceTestResultItem are used as property types, so only those change generated encoding; the rest are sealed for consistency so the same bug cannot reappear when a nested property is added. - Migrate the 9 remaining [DataContract] server configs to [DataType] sealed partial classes and drop the DataContractSerializer-only [OnDeserializing] hooks and now-unused usings. All 9 are empty (no [DataMember] properties) and none is ever deserialized - each node manager hardcodes m_configuration = null and falls back to a new instance - so this is a format change only. - Add the OPCFoundation.NetStandard.Opc.Ua.SourceGeneration analyzer reference to the 9 projects that need it. Analyzers do not flow through ProjectReference, and HistoricalAccess Server already carried [DataType] annotations that no generator ever acted on. UA Sample Controls deliberately keeps no analyzer reference: PerformanceTestResult is still round-tripped through DataContractSerializer in PerformanceTestDlg, and its TestCaseResults is a private List<T> rather than ArrayOf<T>. Only the sealing is applied there and its live serializer hooks are left intact. Verified: AlarmCondition config now decodes all 8 areas across three levels of nesting (0 before), the generated encoder switched from WriteEncodeableArrayAsExtensionObjects to WriteEncodeableArray, and ParseExtension<BoilerServerConfiguration> now returns an IEncodeable instance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Now that these config types are source-generated IEncodeable implementations, ParseExtension<T> can bind them again. It constrains T to IEncodeable, new(), which is why these call sites were stubbed out to m_configuration = null during the 2.0 migration. Replace the stub with the real call in nine node managers: Boiler, DataAccess, DataTypes, Empty, HistoricalEvents, Methods, PerfTest, SimpleEvents and HistoricalAccess. The existing "use suitable defaults if no configuration exists" fallback is left in place. Two of them did not actually bind and needed their namespaces reconciled first; both failed silently, falling back to defaults rather than raising an error: - HistoricalAccess used a bare [DataType], so its namespace defaulted to urn:quickstarts.historicalaccessserver while the shipped config declares http://opcfoundation.org/Quickstarts/HistoricalAccess. This is the only one of the nine that carries real data, so ArchiveRoot silently stayed null instead of reading ".\Archive". Bound explicitly to Namespaces.HistoricalAccess. - DataTypesServer.Config.xml declared xmlns="http://somecompany.com/DataTypes", which matches no constant in the project. Corrected to urn:localhost:somecompany.com:DataTypesServer, the value of Quickstarts.DataTypes.Namespaces.DataTypes and the server's own primary node namespace, making it consistent with the other eight samples. AggregationServerConfiguration is deliberately untouched: it has no node manager field, no consumer, and no element in Quickstarts.AggregationServer.Config.xml, so there is no ParseExtension call to restore. Verified by loading each shipped Config.xml and calling ParseExtension<T>: all nine now bind and return an IEncodeable instance, and HistoricalAccess reads ArchiveRoot = ".\Archive". Reverting either namespace fix reproduces the silent null on that sample. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mple-configs-to-datatype
Merging master brought in the package bump from #785, but the nine SourceGeneration analyzer references this branch adds exist only here, so the merge left them pinned to 2.0.158.59919-preview - a version the opcua-preview feed no longer publishes. Git could not flag it as a conflict because nothing on master touches those lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
AreaConfiguration(AlarmCondition server) is a non-sealedpartial class. The source generator's defaultStructureHandling.Autoresolves to inline encoding only when the field's type issealedand has noIEncodeablebase — otherwise it wraps the value in anExtensionObjectto allow subtyping.AreaConfigurationis the element type of two arrays, neither of which setsStructureHandling:AlarmConditionServerConfiguration.AreasAreaConfiguration.SubAreas(self-recursive)So both were generated as
WriteEncodeableArrayAsExtensionObjects/ReadEncodeableArrayAsExtensionObjects, while the shippedAlarmConditionServer.Config.xmlstores plain nested<AreaConfiguration>elements with noExtensionObject/TypeIdwrappers.The result was a silent failure — the area tree decoded as empty, no exception:
Changes
Seal the 10 existing
[DataType]types.AreaConfiguration,MemoryBufferInstanceandPerformanceTestResultItemare used as property types, so only those change generated encoding. The rest are sealed for consistency, so the same bug cannot reappear when someone adds a nested property. None of these types is subclassed anywhere in the repo, so sealing is compile-safe.Migrate the 9 remaining
[DataContract]server configs to[DataType]sealed partial classes — Aggregation, Boiler, DataAccess, DataTypes, Empty, HistoricalEvents, Methods, PerfTest, SimpleEvents. The[OnDeserializing]Initialize(StreamingContext)hooks and now-unusedSystem.Runtime.Serialization/System.ServiceModelusings are dropped, and the missingusing Opc.Ua;added. The same residual scaffolding is cleaned from the already-migrated configs (Views, UserAuthentication, MemoryBuffer, TestData, HistoricalAccess).Add the
OPCFoundation.NetStandard.Opc.Ua.SourceGenerationanalyzer reference to 9 projects.[DataType]without the analyzer generates nothing. Analyzers do not flow throughProjectReference, so nothing else supplied it. Note thatHistoricalAccess Serveralready carried[DataType]/[DataTypeField]annotations that no generator ever acted on. No package version bump — the pinned2.0.158.59919-previewalready contains the sealed/StructureHandlingsupport.Re-enable
ParseExtension<T>in nine node managers. These call sites had been stubbed tom_configuration = null;during the 2.0 migration, becauseParseExtension<T>constrainsTtoIEncodeable, new()and the configs were plain[DataContract]POCOs. Now that they are source-generatedIEncodeabletypes, the real call is restored in Boiler, DataAccess, DataTypes, Empty, HistoricalEvents, Methods, PerfTest, SimpleEvents and HistoricalAccess. The existing "use suitable defaults if no configuration exists" fallback is kept.Two of those did not actually bind and needed their namespaces reconciled first. Both failed silently, falling back to defaults rather than erroring:
[DataType], so its namespace defaulted tourn:quickstarts.historicalaccessserver, while the shipped config declareshttp://opcfoundation.org/Quickstarts/HistoricalAccess. This is the only one of the nine carrying real data, soArchiveRootsilently stayednullinstead of reading.\Archive. Now bound explicitly toNamespaces.HistoricalAccess.xmlns="http://somecompany.com/DataTypes", which matches no constant in the project. Corrected tourn:localhost:somecompany.com:DataTypesServer— the value ofQuickstarts.DataTypes.Namespaces.DataTypesand the server's own primary node namespace — making it consistent with the other eight samples.Deliberately not changed
UA Sample Controlsgets no analyzer reference.PerformanceTestResultis still actively round-tripped throughDataContractSerializerinPerformanceTestDlg.LoadResults/SaveResults, and itsTestCaseResultsis aprivate List<T>rather thanArrayOf<T>. Only the sealing is applied there, and its live serializer hooks are left intact rather than reshaping unrelated UI sample code.AggregationServerConfigurationkeeps noParseExtensioncall: it has no node manager field, no consumer, and no element inQuickstarts.AggregationServer.Config.xml, so there is nothing to restore. (AggregationServer.csseparately parsesConfiguredEndpointCollection, which is unaffected.)Verification
AlarmConditionServerConfigurationswitched fromWriteEncodeableArrayAsExtensionObjectstoWriteEncodeableArrayfor bothAreasandSubAreas; zeroAsExtensionObjectcalls remain.AlarmConditionServer.Config.xmlthroughParseExtension<AlarmConditionServerConfiguration>()decodes the full three-level tree (Green/Yellow → East/West → Red/Blue with 2SourcePathseach) — 8 areas, versus 0 onmaster.Config.xmlfiles and callingParseExtension<T>returns a boundIEncodeableinstance in every case, andHistoricalAccessreadsArchiveRoot = ".\Archive":nullon that sample, confirming both were required.Note for reviewers
Eight of the nine migrated configs are empty (no fields), so restoring
ParseExtensionchanges no observable behavior for them — it removes dead stubs and makes the samples demonstrate the intended pattern again.HistoricalAccessis the exception: it is a real behavior fix, sinceArchiveRootfrom the config file was previously being ignored.🤖 Generated with Claude Code