mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
9b034f8291
# Description Nullable reference types are now enabled once, in `common.props`, and every project in the repository builds warning-clean with them. **Configuration** - `<Nullable>enable</Nullable>` lives in `common.props`. All per-project `<Nullable>` settings are removed: 178 project files (enable, annotations, and the `disable` in the three Aot test projects). - 419 per-file `#nullable enable`/`restore` directives and the 4 `#nullable disable` directives are removed. - Kept on purpose: - The 5 `<auto-generated>` schema files (`UANodeSet.cs`, `UA Type Dictionary.cs`, `OPCBinarySchema.cs`, `SecuredApplication.cs`, `PInvokeHelper.cs`) keep `#nullable enable`. The compiler ignores the project setting in generated code, so the directive is required there. - `tests/Opc.Ua.Interop.LegacyPeer` keeps `disable`. It builds against the 1.5.378 packages and does not import the repository props. - `docs/DeveloperGuide.md` and `.github/copilot-instructions.md` now describe the central setting. **Migration** (the 43 unset and 13 annotations-only projects, roughly 10k diagnostics) - **Production code: annotated at the declarations.** Covers `tools/Opc.Ua.SourceGeneration.Core`, `src/Opc.Ua.Lds.Server` and the LDS sample. - Members, parameters and returns are nullable where null actually flows. - Null-check helpers carry `[NotNullWhen]` (`IsNull`, `IsValidXmlName`, `IsMethodTypeDesign`). - `CoreUtils.Format`, `Utils.Format`, `ITemplateWriter.WriteLine` take `params object?[]`. `Template.AddReplacement` takes `string?` (rendering already skips null replacements). - `IRegisteredServerStore.RegisterAsync` returns `Task<RegistrationEntry?>`: it returns null when a registration is removed. - Dereferences backed by an invariant (for example, the validated model) use `!`, so runtime behaviour is unchanged. - **Behavioural changes in production code** (small, deliberate): - `NodeSetToModelDesign` throws `InvalidOperationException` when the file is not a NodeSet. Before, it threw a NullReferenceException on the next line. - `ModelDesignValidator` starts its lookup tables empty instead of null. A redundant `if (m_nodes == null) return;` guard is gone. - **Test code: minimal annotations.** - `?` on values that can be null, `!` where the test asserts non-null. - `= null!` for fixture members assigned in SetUp. - `TryGetStructure<T>(out T? x)` where the inferred `T?` broke the `IEncodeable` constraint. - A few redundant `== null` checks on structs (`NodeId`, `ArrayOf<T>`) folded into the adjacent `IsNull`/`Count` checks. **Notes for reviewers** - The diff is large (~1,500 files) but almost entirely mechanical: `?`, `!` and directive removal. The non-annotation changes are listed above. - Known follow-up, not done here: `IModelDesign.Namespaces/TargetNamespace/NamespaceUris` stay non-nullable even though the validator returns null before it loads a dictionary. Making them nullable would ripple through every generator call site. ## Related Issues - Fixes #4439 ## 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. - [ ] 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. The "no new warnings" box covers net10.0 and net48: full non-incremental builds of `UA.slnx`, `tools/SourceGeneration.slnx`, `tools/MigrationAnalyzer.slnx` and the Aot test projects finished with 0 warnings and 0 errors under the repository's warnings-as-errors settings. net9.0 and net8.0 also had no nullable diagnostics, but they were not rebuilt after the final fixes. Tests have not been run locally yet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2264 lines
101 KiB
C#
2264 lines
101 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Opc.Ua.Schema.Model;
|
|
|
|
namespace Opc.Ua.SourceGeneration
|
|
{
|
|
/// <summary>
|
|
/// Emits a typed, intellisense-aware fluent builder facade for the
|
|
/// model's predefined-instance tree. Drops in alongside the existing
|
|
/// <see cref="NodeManagerGenerator"/> output so users can write
|
|
/// <c>b.Boilers.Boiler__1.DrumX001.LIX001.Output.OnRead(...)</c>
|
|
/// inside their <c>Configure</c> partial.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// One typed wrapper class is emitted per instance design that lives
|
|
/// in the manager's predefined-instance tree. Each wrapper exposes
|
|
/// typed accessor properties for its direct children (resolved via
|
|
/// the design <c>Hierarchy</c>):
|
|
/// </para>
|
|
/// <list type="bullet">
|
|
/// <item><description>variables → <c>IVariableBuilder<T></c>;</description></item>
|
|
/// <item><description>methods → a per-method wrapper class with typed
|
|
/// <c>OnCall(Func<…>)</c> overloads;</description></item>
|
|
/// <item><description>objects → the matching child instance wrapper.</description></item>
|
|
/// </list>
|
|
/// <para>
|
|
/// A top-level <c>I{Manager}NodeManagerBuilder</c> interface (extending
|
|
/// <c>Opc.Ua.Server.Fluent.INodeManagerBuilder</c>) carries one
|
|
/// accessor per top-level predefined instance. The
|
|
/// node-manager generator emits both
|
|
/// <c>Configure(INodeManagerBuilder)</c> and
|
|
/// <c>Configure(I{Manager}NodeManagerBuilder)</c> partials so existing
|
|
/// implementations continue to work unchanged.
|
|
/// </para>
|
|
/// </remarks>
|
|
internal sealed class FluentBuilderGenerator : IGenerator
|
|
{
|
|
/// <summary>
|
|
/// Optional override for the manager class name (matches the value
|
|
/// passed to <see cref="NodeManagerGenerator.OverrideClassName"/>).
|
|
/// Defaults to <c>{Prefix}NodeManager</c>.
|
|
/// </summary>
|
|
public string? OverrideManagerClassName { get; init; }
|
|
|
|
/// <summary>
|
|
/// Optional override for the manager namespace (matches the value
|
|
/// passed to <see cref="NodeManagerGenerator.OverrideNamespace"/>).
|
|
/// Defaults to the model's target-namespace prefix.
|
|
/// </summary>
|
|
public string? OverrideManagerNamespace { get; init; }
|
|
|
|
/// <summary>
|
|
/// When <see langword="true"/>, emit the typed
|
|
/// <c>I{Manager}Builder</c> interface, its typed-builder
|
|
/// implementation, and one wrapper class per top-level
|
|
/// predefined instance. When <see langword="false"/> only the
|
|
/// per-ObjectType IComponentAccessor / IPropertyAccessor
|
|
/// extension classes are emitted (the manager-wrapper output is
|
|
/// scoped to models that ship a generated <c>NodeManager</c> —
|
|
/// NodeSet2-only consumers that just want type-level typed
|
|
/// accessors leave this off). Defaults to <see langword="true"/>
|
|
/// for backward compatibility with existing ModelDesign callers.
|
|
/// </summary>
|
|
public bool GenerateManagerWrappers { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// When <see langword="true"/> (the default), emit the
|
|
/// per-ObjectType <c>IComponentAccessor<TState></c> /
|
|
/// <c>IPropertyAccessor<TState></c> extension classes.
|
|
/// Set to <see langword="false"/> when the consumer assembly
|
|
/// does not reference <c>Opc.Ua.Server</c> (the emitted
|
|
/// emitted by <see cref="FluentBuilderGenerator"/> is allowed.
|
|
/// The marker interfaces themselves live in
|
|
/// <c>Opc.Ua.Types</c> (Core); the emitted method bodies
|
|
/// reference <c>Opc.Ua.Server.Fluent.INodeBuilder<T></c>
|
|
/// and would fail to compile there).
|
|
/// </summary>
|
|
public bool EmitFluentAccessors { get; init; } = true;
|
|
|
|
/// <summary>
|
|
/// Initializes a new <see cref="FluentBuilderGenerator"/>.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="context"/> is null.</exception>
|
|
public FluentBuilderGenerator(IGeneratorContext context)
|
|
{
|
|
m_context = context ?? throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IEnumerable<Resource> Emit()
|
|
{
|
|
string nsPrefix = m_context.ModelDesign.TargetNamespace.Prefix;
|
|
string typeStem = nsPrefix.Replace(".", string.Empty, StringComparison.Ordinal);
|
|
string outputNamespace = string.IsNullOrEmpty(OverrideManagerNamespace)
|
|
? nsPrefix
|
|
: OverrideManagerNamespace;
|
|
string managerClassName = string.IsNullOrEmpty(OverrideManagerClassName)
|
|
? typeStem + "NodeManager"
|
|
: OverrideManagerClassName;
|
|
string interfaceName = "I" + managerClassName + "Builder";
|
|
string typedBuilderClassName = managerClassName + "TypedBuilder";
|
|
|
|
// Discover the top-level predefined instances. We always emit
|
|
// the typed manager interface and proxy class — even for
|
|
// models with no predefined instances — because the
|
|
// source-generated NodeManager partial unconditionally
|
|
// references them.
|
|
List<InstanceDesign> roots = GetTopLevelInstances();
|
|
|
|
// Walk every instance under each root and assemble the wrapper
|
|
// class metadata. m_wrappers is keyed by SymbolicId path so
|
|
// child accessors can resolve their target wrapper by id.
|
|
m_wrappers = [];
|
|
m_methodWrappers = [];
|
|
// The wrappers are only emitted with the manager; a model that
|
|
// just gets the per-type accessors must not fail generation over
|
|
// names that nothing will use.
|
|
if (GenerateManagerWrappers)
|
|
{
|
|
foreach (InstanceDesign root in roots)
|
|
{
|
|
CollectInstanceWrappers(root);
|
|
}
|
|
|
|
// Detect naming collisions per containing wrapper. Fail with a
|
|
// diagnostic (mirrored as an InvalidOperationException since we
|
|
// are running outside the Roslyn diagnostic pipeline here).
|
|
ValidateNoCollisions();
|
|
ValidateNoRootCollisions(roots, typedBuilderClassName);
|
|
|
|
// Wire each wrapper to its direct child object/method
|
|
// wrappers so the recursive emitter can walk the tree
|
|
// depth-first and emit nested type declarations.
|
|
LinkChildWrappers();
|
|
}
|
|
|
|
string fileName = Path.Combine(
|
|
m_context.OutputFolder,
|
|
CoreUtils.Format("{0}.FluentBuilders.g.cs",
|
|
string.IsNullOrEmpty(OverrideManagerClassName)
|
|
? nsPrefix
|
|
// Namespace-qualified, like the node manager files, so
|
|
// two bound managers sharing a class name do not
|
|
// overwrite each other's output.
|
|
: outputNamespace + "." + OverrideManagerClassName));
|
|
|
|
using TextWriter writer = m_context.FileSystem.CreateTextWriter(fileName);
|
|
using var templateWriter = new TemplateWriter(writer);
|
|
var template = new Template(templateWriter, FluentBuilderTemplates.File);
|
|
|
|
// Advertise the accessors only when they are emitted, keyed by the
|
|
// model's C# prefix: that is what a downstream accessors-only build
|
|
// compares against to avoid emitting a second, ambiguous set
|
|
// (CS0121). The manager namespace the accessors are placed in when
|
|
// a [NodeManager] binding overrides it is not that prefix.
|
|
template.AddReplacement(
|
|
Tokens.AssemblyAttributes,
|
|
EmitFluentAccessors
|
|
? CoreUtils.Format(
|
|
"[assembly: global::Opc.Ua.ModelFluentAccessorProviderAttribute(\"{0}\", \"{1}\")]",
|
|
SourceGenerationUtils.Escape(m_context.ModelDesign.TargetNamespace.Value),
|
|
SourceGenerationUtils.Escape(nsPrefix))
|
|
: null);
|
|
template.AddReplacement(Tokens.NamespacePrefix, outputNamespace);
|
|
|
|
// Render the typed manager interface, the typed manager
|
|
// implementation, and one wrapper class per instance / method
|
|
// into the body slot. The body is emitted by a single
|
|
// load-template callback (the templating engine treats the
|
|
// returned null TemplateString as "nothing more to emit").
|
|
object[] bodyTargets = ["body"];
|
|
template.AddReplacement(
|
|
Tokens.ListOfTypes,
|
|
bodyTargets,
|
|
onLoad: ctx =>
|
|
{
|
|
if (GenerateManagerWrappers)
|
|
{
|
|
EmitManagerInterface(ctx.Out, interfaceName, roots);
|
|
EmitTypedManagerImpl(
|
|
ctx.Out,
|
|
interfaceName,
|
|
typedBuilderClassName,
|
|
managerClassName,
|
|
roots);
|
|
|
|
// Walk top-level instance wrappers depth-first so each
|
|
// child object/method wrapper is emitted as a nested
|
|
// type inside its parent. Top-level wrappers (those
|
|
// whose parent path is empty) live at namespace scope.
|
|
foreach (InstanceWrapper top in m_wrappers.Values
|
|
.Where(w => w.ParentKey == null)
|
|
.OrderBy(w => w.LeafName, StringComparer.Ordinal))
|
|
{
|
|
EmitInstanceWrapper(ctx.Out, top, indent: string.Empty);
|
|
}
|
|
}
|
|
|
|
// Emit per-ObjectType IComponentAccessor /
|
|
// IPropertyAccessor extension classes so consumers
|
|
// can walk a typed INodeBuilder<TState> tree
|
|
// without spelling out browse paths or
|
|
// QualifiedNames. Always runs (independent of
|
|
// GenerateManagerWrappers) so NodeSet2-only
|
|
// consumers still get the typed accessors.
|
|
// Suppressed when the consumer assembly does not
|
|
// reference Opc.Ua.Server (controlled by the
|
|
// EmitFluentAccessors flag).
|
|
if (EmitFluentAccessors)
|
|
{
|
|
EmitObjectTypeAccessors(ctx.Out);
|
|
}
|
|
return null;
|
|
});
|
|
|
|
template.Render();
|
|
return [fileName.AsTextFileResource()];
|
|
}
|
|
|
|
// Discovery
|
|
/// <summary>
|
|
/// Returns the model's top-level instance designs — those whose
|
|
/// <see cref="NodeDesign.Parent"/> is null and which sit in the
|
|
/// manager's predefined-instance tree (instances, not types).
|
|
/// </summary>
|
|
private List<InstanceDesign> GetTopLevelInstances()
|
|
{
|
|
var result = new List<InstanceDesign>();
|
|
foreach (NodeDesign node in m_context.ModelDesign.GetNodeDesigns())
|
|
{
|
|
if (node is not InstanceDesign instance)
|
|
{
|
|
continue;
|
|
}
|
|
if (instance.Parent != null)
|
|
{
|
|
continue;
|
|
}
|
|
if (m_context.ModelDesign.IsExcluded(instance))
|
|
{
|
|
continue;
|
|
}
|
|
if (instance.NotInAddressSpace)
|
|
{
|
|
continue;
|
|
}
|
|
if (instance.IsDeclaration)
|
|
{
|
|
continue;
|
|
}
|
|
// DataTypeEncoding objects (Default Binary/XML/JSON) hang off
|
|
// their DataType rather than the instance tree, so they surface
|
|
// here as parent-less "root" instances that all share one of the
|
|
// reserved symbolic names (DefaultBinary/DefaultXml/DefaultJson).
|
|
// They are encoding metadata, never fluent-wired, so skip them —
|
|
// otherwise a model with more than one structure emits colliding
|
|
// typed-builder accessors.
|
|
if (instance.TypeDefinition?.Name is "DataTypeEncodingType")
|
|
{
|
|
continue;
|
|
}
|
|
if (instance.Hierarchy == null)
|
|
{
|
|
continue;
|
|
}
|
|
result.Add(instance);
|
|
}
|
|
result.Sort(static (a, b) => string.CompareOrdinal(
|
|
a.SymbolicId?.Name,
|
|
b.SymbolicId?.Name));
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walks <paramref name="root"/>'s hierarchy and registers a
|
|
/// wrapper for every non-method instance plus a method wrapper for
|
|
/// every method.
|
|
/// </summary>
|
|
private void CollectInstanceWrappers(InstanceDesign root)
|
|
{
|
|
if (root.Hierarchy == null)
|
|
{
|
|
return;
|
|
}
|
|
// Build a parent-path → list of direct children mapping. The
|
|
// hierarchy keys are constructed by joining segments with
|
|
// <see cref="NodeDesign.PathChar"/>; segment names themselves
|
|
// can contain underscores (e.g. <c>Boiler__1</c> for the
|
|
// browse name <c>Boiler #1</c>) so we cannot naively split on
|
|
// the path char. Instead we derive the parent path by stripping
|
|
// the child node's own <c>SymbolicName.Name</c> (plus the
|
|
// separator) from its <c>RelativePath</c> — the same trick
|
|
// used by <see cref="NodeStateGenerator"/>.
|
|
var directChildren = new Dictionary<string, List<HierarchyNode>>(StringComparer.Ordinal);
|
|
foreach (KeyValuePair<string, HierarchyNode> entry in root.Hierarchy.Nodes)
|
|
{
|
|
string path = entry.Key ?? string.Empty;
|
|
if (path.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
HierarchyNode hnode = entry.Value;
|
|
if (hnode?.Instance?.SymbolicName?.Name is not string segment ||
|
|
segment.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
int trimLen = segment.Length + 1;
|
|
string parent;
|
|
if (path.Length == segment.Length)
|
|
{
|
|
// Top-level child of the root (no preceding parent path).
|
|
parent = string.Empty;
|
|
}
|
|
else if (path.Length < trimLen)
|
|
{
|
|
// Malformed — skip.
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
parent = path[..^trimLen];
|
|
}
|
|
if (!directChildren.TryGetValue(parent, out List<HierarchyNode>? bucket))
|
|
{
|
|
bucket = [];
|
|
directChildren[parent] = bucket;
|
|
}
|
|
bucket.Add(hnode);
|
|
}
|
|
|
|
// Emit one wrapper per non-method instance (root + descendants).
|
|
foreach (KeyValuePair<string, HierarchyNode> entry in root.Hierarchy.Nodes)
|
|
{
|
|
if (entry.Value?.Instance is not NodeDesign nd)
|
|
{
|
|
continue;
|
|
}
|
|
if (m_context.ModelDesign.IsExcluded(nd))
|
|
{
|
|
continue;
|
|
}
|
|
// Skip method instances — methods get their own wrapper
|
|
// class, registered separately below.
|
|
if (entry.Value.Instance is MethodDesign method)
|
|
{
|
|
RegisterMethodWrapper(root, entry.Key, method);
|
|
continue;
|
|
}
|
|
// Skip variables — they are surfaced as IVariableBuilder<T>
|
|
// accessors on their parent wrapper, not as standalone
|
|
// wrapper classes.
|
|
if (entry.Value.Instance is VariableDesign)
|
|
{
|
|
continue;
|
|
}
|
|
if (entry.Value.Instance is not ObjectDesign)
|
|
{
|
|
continue;
|
|
}
|
|
RegisterInstanceWrapper(root, entry.Key, entry.Value, directChildren);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers an <c>InstanceWrapper</c> for the supplied
|
|
/// node. Idempotent — if the wrapper has already been registered
|
|
/// (e.g. via a sibling root) the existing entry is reused.
|
|
/// </summary>
|
|
private void RegisterInstanceWrapper(
|
|
InstanceDesign root,
|
|
string relativePath,
|
|
HierarchyNode hnode,
|
|
Dictionary<string, List<HierarchyNode>> directChildren)
|
|
{
|
|
string key = ComposeKey(root, relativePath);
|
|
if (m_wrappers.ContainsKey(key))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string leafName = ResolveLeafName(root, relativePath, hnode.Instance);
|
|
string? parentKey = ResolveParentKey(root, relativePath, leafName);
|
|
string className = ComposeWrapperClassName(leafName, suffix: "Builder");
|
|
var wrapper = new InstanceWrapper
|
|
{
|
|
Key = key,
|
|
ClassName = className,
|
|
LeafName = leafName,
|
|
ParentKey = parentKey,
|
|
NodeStateType = ResolveStateClrType(hnode.Instance),
|
|
BrowseNamespaceUri = ResolveNodeBrowseNamespace(hnode.Instance),
|
|
SupportsPublish = QualifiesAsEventNotifier(hnode.Instance),
|
|
Children = [],
|
|
ChildObjectKeys = [],
|
|
ChildMethodKeys = []
|
|
};
|
|
|
|
// Children resolved relative to this node.
|
|
if (directChildren.TryGetValue(relativePath, out List<HierarchyNode>? kids))
|
|
{
|
|
foreach (HierarchyNode kid in kids)
|
|
{
|
|
if (kid?.Instance == null || m_context.ModelDesign.IsExcluded(kid.Instance))
|
|
{
|
|
continue;
|
|
}
|
|
string childKey = ComposeKey(root, kid.RelativePath);
|
|
string accessorName = GetAccessorName(kid.Instance);
|
|
string browseName = GetBrowseName(kid.Instance);
|
|
string browseNsUri = ResolveNodeBrowseNamespace(kid.Instance);
|
|
string childLeaf = ResolveLeafName(root, kid.RelativePath, kid.Instance);
|
|
var child = new ChildAccessor
|
|
{
|
|
AccessorName = accessorName,
|
|
BrowseName = browseName,
|
|
BrowseNamespaceUri = browseNsUri
|
|
};
|
|
|
|
switch (kid.Instance)
|
|
{
|
|
case VariableDesign var:
|
|
child.Kind = ChildKind.Variable;
|
|
child.ValueClrType = GetVariableValueClrType(var);
|
|
break;
|
|
case MethodDesign:
|
|
child.Kind = ChildKind.Method;
|
|
// Lexical scope: methods are emitted as nested
|
|
// classes inside the parent wrapper, so the
|
|
// simple leaf name resolves correctly here.
|
|
child.WrapperClassName = ComposeWrapperClassName(
|
|
childLeaf, suffix: "MethodBuilder");
|
|
break;
|
|
case ObjectDesign:
|
|
child.Kind = ChildKind.Object;
|
|
// Lexical scope: object child wrappers are
|
|
// nested classes; the simple leaf name resolves
|
|
// through the enclosing parent wrapper.
|
|
child.WrapperClassName = ComposeWrapperClassName(
|
|
childLeaf, suffix: "Builder");
|
|
child.ChildKey = childKey;
|
|
child.ChildStateType = ResolveStateClrType(kid.Instance);
|
|
break;
|
|
default:
|
|
// Properties / unknowns: skip for v1.
|
|
continue;
|
|
}
|
|
wrapper.Children.Add(child);
|
|
}
|
|
}
|
|
|
|
m_wrappers[key] = wrapper;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a <c>MethodWrapper</c> for the supplied method
|
|
/// design. Resolves typed argument shapes from the method's
|
|
/// <c>InputArguments</c>/<c>OutputArguments</c>.
|
|
/// </summary>
|
|
private void RegisterMethodWrapper(
|
|
InstanceDesign root,
|
|
string relativePath,
|
|
MethodDesign method)
|
|
{
|
|
string key = ComposeKey(root, relativePath);
|
|
if (m_methodWrappers.ContainsKey(key))
|
|
{
|
|
return;
|
|
}
|
|
|
|
string leafName = ResolveLeafName(root, relativePath, method);
|
|
string? parentKey = ResolveParentKey(root, relativePath, leafName);
|
|
string className = ComposeWrapperClassName(leafName, suffix: "MethodBuilder");
|
|
MethodDesign effectiveMethod = method.IsOverridden()
|
|
? (MethodDesign)method.GetMergedInstance()
|
|
: method;
|
|
(Parameter[] inputs, Parameter[] outputs) =
|
|
MethodDesignArgumentResolver.ResolveMethodArguments(effectiveMethod);
|
|
m_methodWrappers[key] = new MethodWrapper
|
|
{
|
|
Key = key,
|
|
ClassName = className,
|
|
LeafName = leafName,
|
|
ParentKey = parentKey,
|
|
Inputs = inputs,
|
|
Outputs = outputs
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wires each wrapper to its direct child object/method wrappers
|
|
/// so the recursive emitter can walk the tree depth-first. Sorts
|
|
/// siblings by leaf name (ordinal) so generation is deterministic.
|
|
/// </summary>
|
|
private void LinkChildWrappers()
|
|
{
|
|
foreach (InstanceWrapper child in m_wrappers.Values)
|
|
{
|
|
if (child.ParentKey == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (m_wrappers.TryGetValue(child.ParentKey, out InstanceWrapper? parent))
|
|
{
|
|
parent.ChildObjectKeys.Add(child.Key!);
|
|
}
|
|
}
|
|
foreach (MethodWrapper method in m_methodWrappers.Values)
|
|
{
|
|
if (method.ParentKey == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (m_wrappers.TryGetValue(method.ParentKey, out InstanceWrapper? parent))
|
|
{
|
|
parent.ChildMethodKeys.Add(method.Key!);
|
|
}
|
|
}
|
|
foreach (InstanceWrapper wrapper in m_wrappers.Values)
|
|
{
|
|
wrapper.ChildObjectKeys.Sort((a, b) => string.CompareOrdinal(
|
|
m_wrappers[a].LeafName,
|
|
m_wrappers[b].LeafName));
|
|
wrapper.ChildMethodKeys.Sort((a, b) => string.CompareOrdinal(
|
|
m_methodWrappers[a].LeafName,
|
|
m_methodWrappers[b].LeafName));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that no two children of the same wrapper sanitize to
|
|
/// the same C# accessor identifier.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">Two children of the same wrapper resolve to the same C# accessor name.</exception>
|
|
private void ValidateNoCollisions()
|
|
{
|
|
foreach (InstanceWrapper wrapper in m_wrappers.Values)
|
|
{
|
|
// The wrapper itself declares Builder and Node (and Publish when
|
|
// it is an event notifier); a child named after one of those
|
|
// would not compile.
|
|
var reserved = new HashSet<string>(s_reservedWrapperMembers, StringComparer.Ordinal);
|
|
if (wrapper.SupportsPublish)
|
|
{
|
|
reserved.Add("Publish");
|
|
}
|
|
|
|
// Every name declared in the wrapper's body: the child accessor
|
|
// properties and the nested wrapper types of object and method
|
|
// children share one member namespace (CS0102), and none of them
|
|
// may carry the name of the enclosing wrapper class (CS0542).
|
|
var seen = new Dictionary<string, ChildAccessor>(StringComparer.Ordinal);
|
|
foreach (ChildAccessor child in wrapper.Children!)
|
|
{
|
|
if (reserved.Contains(child.AccessorName!) ||
|
|
string.Equals(
|
|
child.AccessorName,
|
|
wrapper.ClassName,
|
|
StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException(CoreUtils.Format(
|
|
"Fluent builder generation: child '{0}' on '{1}' " +
|
|
"sanitizes to the C# accessor '{2}', which the " +
|
|
"generated wrapper already declares. Rename the " +
|
|
"child in the design.",
|
|
child.BrowseName,
|
|
wrapper.ClassName,
|
|
child.AccessorName));
|
|
}
|
|
|
|
if (seen.TryGetValue(child.AccessorName!, out ChildAccessor? existing))
|
|
{
|
|
throw new InvalidOperationException(CoreUtils.Format(
|
|
"Fluent builder generation: children '{0}' and '{1}' on '{2}' both sanitize to the same C# accessor '{3}'. Rename one of the children in the design.",
|
|
existing.BrowseName,
|
|
child.BrowseName,
|
|
wrapper.ClassName,
|
|
child.AccessorName));
|
|
}
|
|
seen[child.AccessorName!] = child;
|
|
}
|
|
|
|
foreach (ChildAccessor child in wrapper.Children)
|
|
{
|
|
if (child.Kind is not (ChildKind.Object or ChildKind.Method) ||
|
|
string.IsNullOrEmpty(child.WrapperClassName))
|
|
{
|
|
continue;
|
|
}
|
|
string nestedType = child.WrapperClassName;
|
|
if (reserved.Contains(nestedType) ||
|
|
string.Equals(nestedType, wrapper.ClassName, StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException(CoreUtils.Format(
|
|
"Fluent builder generation: child '{0}' on '{1}' " +
|
|
"gets the nested wrapper type '{2}', which the " +
|
|
"generated wrapper already declares. Rename the " +
|
|
"child in the design.",
|
|
child.BrowseName,
|
|
wrapper.ClassName,
|
|
nestedType));
|
|
}
|
|
if (seen.TryGetValue(nestedType, out ChildAccessor? existing))
|
|
{
|
|
throw new InvalidOperationException(CoreUtils.Format(
|
|
"Fluent builder generation: the nested wrapper type '{0}' " +
|
|
"of child '{1}' on '{2}' collides with the member of " +
|
|
"child '{3}'. Rename one of the children in the design.",
|
|
nestedType,
|
|
child.BrowseName,
|
|
wrapper.ClassName,
|
|
existing.BrowseName));
|
|
}
|
|
seen[nestedType] = child;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the typed top-level accessors of the manager builder
|
|
/// neither collide with each other nor with the members the typed
|
|
/// builder forwards from <c>INodeManagerBuilder</c> (for example a root
|
|
/// instance named <c>Context</c>), nor with the builder class itself.
|
|
/// </summary>
|
|
/// <exception cref="InvalidOperationException">A root accessor collides.</exception>
|
|
private void ValidateNoRootCollisions(
|
|
IReadOnlyList<InstanceDesign> roots,
|
|
string typedBuilderClassName)
|
|
{
|
|
var seen = new Dictionary<string, InstanceDesign>(StringComparer.Ordinal);
|
|
foreach (InstanceDesign root in roots)
|
|
{
|
|
if (!m_wrappers.ContainsKey(ComposeKey(root, string.Empty)))
|
|
{
|
|
continue;
|
|
}
|
|
string accessor = GetAccessorName(root);
|
|
if (s_typedBuilderMembers.Contains(accessor) ||
|
|
string.Equals(accessor, typedBuilderClassName, StringComparison.Ordinal))
|
|
{
|
|
throw new InvalidOperationException(CoreUtils.Format(
|
|
"Fluent builder generation: the predefined instance '{0}' " +
|
|
"sanitizes to the C# accessor '{1}', which the generated " +
|
|
"'{2}' already declares. Rename the instance in the design.",
|
|
GetBrowseName(root),
|
|
accessor,
|
|
typedBuilderClassName));
|
|
}
|
|
if (seen.TryGetValue(accessor, out InstanceDesign? existing))
|
|
{
|
|
throw new InvalidOperationException(CoreUtils.Format(
|
|
"Fluent builder generation: the predefined instances '{0}' " +
|
|
"and '{1}' both sanitize to the C# accessor '{2}'. Rename " +
|
|
"one of them in the design.",
|
|
GetBrowseName(existing),
|
|
GetBrowseName(root),
|
|
accessor));
|
|
}
|
|
seen[accessor] = root;
|
|
}
|
|
}
|
|
|
|
// Emission
|
|
/// <summary>
|
|
/// Emits the typed manager interface declaration with one accessor
|
|
/// per top-level predefined instance.
|
|
/// </summary>
|
|
private void EmitManagerInterface(
|
|
ITemplateWriter writer,
|
|
string interfaceName,
|
|
IReadOnlyList<InstanceDesign> roots)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine("/// <summary>");
|
|
writer.WriteLine(
|
|
"/// Source-generated typed sibling of" +
|
|
" <see cref=\"global::Opc.Ua.Server.Fluent.INodeManagerBuilder\"/>");
|
|
writer.WriteLine(
|
|
"/// that surfaces typed accessors for the predefined-instance tree.");
|
|
writer.WriteLine("/// </summary>");
|
|
writer.WriteLine(
|
|
"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{0}\", \"{1}\")]",
|
|
ToolName,
|
|
ToolVersion);
|
|
writer.WriteLine("internal interface {0} : global::Opc.Ua.Server.Fluent.INodeManagerBuilder", interfaceName);
|
|
writer.WriteLine("{");
|
|
foreach (InstanceDesign root in roots)
|
|
{
|
|
string accessor = GetAccessorName(root);
|
|
string wrapperKey = ComposeKey(root, string.Empty);
|
|
if (!m_wrappers.TryGetValue(wrapperKey, out InstanceWrapper? wrapper))
|
|
{
|
|
continue;
|
|
}
|
|
writer.WriteLine(" /// <summary>Resolves the predefined instance <c>{0}</c>.</summary>",
|
|
EscapeXmlDoc(GetBrowseName(root)));
|
|
writer.WriteLine(" {0} {1} {{ get; }}", wrapper.ClassName, accessor);
|
|
}
|
|
writer.WriteLine("}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the internal sealed implementation that wraps the runtime
|
|
/// fluent <c>NodeManagerBuilder</c> and adds typed top-level
|
|
/// accessors.
|
|
/// </summary>
|
|
private void EmitTypedManagerImpl(
|
|
ITemplateWriter writer,
|
|
string interfaceName,
|
|
string className,
|
|
string managerClassName,
|
|
IReadOnlyList<InstanceDesign> roots)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine("/// <summary>");
|
|
writer.WriteLine(
|
|
"/// Internal proxy that wraps the runtime fluent" +
|
|
" <c>NodeManagerBuilder</c>");
|
|
writer.WriteLine("/// to surface the typed <see cref=\"{0}\"/> facade.", interfaceName);
|
|
writer.WriteLine("/// </summary>");
|
|
writer.WriteLine(
|
|
"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{0}\", \"{1}\")]",
|
|
ToolName,
|
|
ToolVersion);
|
|
writer.WriteLine("internal sealed class {0} : {1}", className, interfaceName);
|
|
writer.WriteLine("{");
|
|
writer.WriteLine(
|
|
" private readonly global::Opc.Ua.Server.Fluent.INodeManagerBuilder __inner;");
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
" internal {0}(global::Opc.Ua.Server.Fluent.INodeManagerBuilder inner)",
|
|
className);
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(
|
|
" __inner = inner ?? throw new global::System.ArgumentNullException(nameof(inner));");
|
|
writer.WriteLine(" }");
|
|
writer.WriteLine();
|
|
|
|
// Pass-through INodeManagerBuilder members. Each line is broken
|
|
// explicitly to satisfy the repository line-length analyzer
|
|
// (RCS0056, max 120 chars).
|
|
EmitPassThroughProperty(writer, "global::Opc.Ua.ISystemContext", "Context");
|
|
EmitPassThroughProperty(writer, "global::Opc.Ua.Server.IAsyncNodeManager", "NodeManager");
|
|
EmitPassThroughProperty(writer, "global::Opc.Ua.Server.Fluent.IFluentDispatcher", "Dispatcher");
|
|
EmitPassThroughProperty(writer, "ushort", "DefaultNamespaceIndex");
|
|
|
|
writer.WriteLine();
|
|
writer.WriteLine(" /// <inheritdoc/>");
|
|
writer.WriteLine(" public void Import(");
|
|
writer.WriteLine(" global::Opc.Ua.Export.UANodeSet nodeSet,");
|
|
writer.WriteLine(
|
|
" global::Opc.Ua.Server.Nodes.INodeSetImportFactoryProvider? factoryProvider = null)");
|
|
writer.WriteLine(" => __inner.Import(nodeSet, factoryProvider);");
|
|
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder", "Node",
|
|
"string browsePath", "browsePath");
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<TState>", "Node",
|
|
"string browsePath", "browsePath");
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder", "Node",
|
|
"global::Opc.Ua.NodeId nodeId", "nodeId");
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<TState>", "Node",
|
|
"global::Opc.Ua.NodeId nodeId", "nodeId");
|
|
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder", "NodeFromTypeId",
|
|
"global::Opc.Ua.NodeId typeDefinitionId", "typeDefinitionId");
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder", "NodeFromTypeId",
|
|
"global::Opc.Ua.NodeId typeDefinitionId, global::Opc.Ua.QualifiedName browseName",
|
|
"typeDefinitionId, browseName");
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<TState>", "NodeFromTypeId",
|
|
"global::Opc.Ua.NodeId typeDefinitionId", "typeDefinitionId");
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<TState>", "NodeFromTypeId",
|
|
"global::Opc.Ua.NodeId typeDefinitionId, global::Opc.Ua.QualifiedName browseName",
|
|
"typeDefinitionId, browseName");
|
|
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "Variable",
|
|
"string browsePath", "browsePath", typeArg: "TValue", noConstraint: true);
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "Variable",
|
|
"global::Opc.Ua.NodeId nodeId", "nodeId", typeArg: "TValue", noConstraint: true);
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "VariableFromTypeId",
|
|
"global::Opc.Ua.NodeId typeDefinitionId", "typeDefinitionId",
|
|
typeArg: "TValue", noConstraint: true);
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "VariableFromTypeId",
|
|
"global::Opc.Ua.NodeId typeDefinitionId, global::Opc.Ua.QualifiedName browseName",
|
|
"typeDefinitionId, browseName",
|
|
typeArg: "TValue", noConstraint: true);
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "VariableFromDataTypeId",
|
|
"global::Opc.Ua.NodeId dataTypeId", "dataTypeId",
|
|
typeArg: "TValue", noConstraint: true);
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "VariableFromDataTypeId",
|
|
"global::Opc.Ua.NodeId dataTypeId, global::Opc.Ua.QualifiedName browseName",
|
|
"dataTypeId, browseName",
|
|
typeArg: "TValue", noConstraint: true);
|
|
|
|
// Node-creation pass-throughs.
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<TState>", "Add",
|
|
"TState node, global::Opc.Ua.NodeId parentId = default", "node, parentId");
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<TState>", "Add",
|
|
"global::System.Func<global::Opc.Ua.NodeState?, TState> factory," +
|
|
" global::Opc.Ua.NodeId parentId = default",
|
|
"factory, parentId");
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<TState>", "AddRoot",
|
|
"TState node", "node");
|
|
EmitPassThroughMethod(writer,
|
|
"bool", "TryGetNode",
|
|
"global::Opc.Ua.NodeId nodeId, out global::Opc.Ua.NodeState? node",
|
|
"nodeId, out node");
|
|
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.FolderState>", "AddFolder",
|
|
"string browseName, global::Opc.Ua.NodeId parentId = default",
|
|
"browseName, parentId");
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.FolderState>", "AddFolder",
|
|
"global::Opc.Ua.QualifiedName browseName, global::Opc.Ua.NodeId parentId = default",
|
|
"browseName, parentId");
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.BaseObjectState>", "AddObject",
|
|
"string browseName, global::Opc.Ua.NodeId parentId = default," +
|
|
" global::Opc.Ua.NodeId typeDefinitionId = default",
|
|
"browseName, parentId, typeDefinitionId");
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.BaseObjectState>", "AddObject",
|
|
"global::Opc.Ua.QualifiedName browseName, global::Opc.Ua.NodeId parentId = default," +
|
|
" global::Opc.Ua.NodeId typeDefinitionId = default",
|
|
"browseName, parentId, typeDefinitionId");
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "AddVariable",
|
|
"string browseName, global::Opc.Ua.NodeId parentId = default",
|
|
"browseName, parentId", typeArg: "TValue", noConstraint: true);
|
|
EmitPassThroughGenericMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.IVariableBuilder<TValue>", "AddVariable",
|
|
"global::Opc.Ua.QualifiedName browseName, global::Opc.Ua.NodeId parentId = default",
|
|
"browseName, parentId", typeArg: "TValue", noConstraint: true);
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.MethodState>", "AddMethod",
|
|
"string browseName, global::Opc.Ua.NodeId parentId = default",
|
|
"browseName, parentId");
|
|
EmitPassThroughMethod(writer,
|
|
"global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.MethodState>", "AddMethod",
|
|
"global::Opc.Ua.QualifiedName browseName, global::Opc.Ua.NodeId parentId = default",
|
|
"browseName, parentId");
|
|
// Typed top-level accessors.
|
|
foreach (InstanceDesign root in roots)
|
|
{
|
|
string accessor = GetAccessorName(root);
|
|
string wrapperKey = ComposeKey(root, string.Empty);
|
|
if (!m_wrappers.TryGetValue(wrapperKey, out InstanceWrapper? wrapper))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_ = GetBrowseName(root);
|
|
// The NodeId lives in the node's own namespace (SymbolicId),
|
|
// which differs from the BrowseName namespace (SymbolicName)
|
|
// for a NodeSet instance named in a companion namespace.
|
|
string nsUri = ResolveNodeIdNamespace(root);
|
|
writer.WriteLine();
|
|
writer.WriteLine(" /// <inheritdoc/>");
|
|
writer.WriteLine(" public {0} {1}", wrapper.ClassName, accessor);
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(" get");
|
|
writer.WriteLine(" {");
|
|
writer.WriteLine(" ushort __ns = __inner.Context.NamespaceUris.GetIndexOrAppend(\"{0}\");",
|
|
SourceGenerationUtils.Escape(nsUri));
|
|
writer.WriteLine(" return new {0}(__inner.Node<{1}>(new global::Opc.Ua.NodeId({2}, __ns)));",
|
|
wrapper.ClassName,
|
|
wrapper.NodeStateType,
|
|
EmitNodeIdConstructorArg(root));
|
|
writer.WriteLine(" }");
|
|
writer.WriteLine(" }");
|
|
}
|
|
|
|
writer.WriteLine("}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits a <c>{Type} {Name} => __inner.{Name};</c> property forwarder
|
|
/// across two output lines so the result fits within RCS0056's 120
|
|
/// character ceiling for any plausible runtime type name.
|
|
/// </summary>
|
|
private static void EmitPassThroughProperty(
|
|
ITemplateWriter writer,
|
|
string returnType,
|
|
string memberName)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine(" /// <inheritdoc/>");
|
|
writer.WriteLine(" public {0} {1}", returnType, memberName);
|
|
writer.WriteLine(" => __inner.{0};", memberName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits a non-generic pass-through method forwarder broken across
|
|
/// multiple lines (signature, opener, body, closer).
|
|
/// </summary>
|
|
private static void EmitPassThroughMethod(
|
|
ITemplateWriter writer,
|
|
string returnType,
|
|
string memberName,
|
|
string parameterList,
|
|
string callArguments)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine(" /// <inheritdoc/>");
|
|
writer.WriteLine(" public {0} {1}({2})", returnType, memberName, parameterList);
|
|
writer.WriteLine(" => __inner.{0}({1});", memberName, callArguments);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits a generic pass-through method forwarder broken across
|
|
/// multiple lines. Defaults to <c>where TState : NodeState</c>;
|
|
/// pass <paramref name="noConstraint"/> = <c>true</c> to suppress
|
|
/// the constraint and <paramref name="typeArg"/> to use a custom
|
|
/// type-parameter name (e.g. <c>TValue</c>).
|
|
/// </summary>
|
|
private static void EmitPassThroughGenericMethod(
|
|
ITemplateWriter writer,
|
|
string returnType,
|
|
string memberName,
|
|
string parameterList,
|
|
string callArguments,
|
|
string typeArg = "TState",
|
|
bool noConstraint = false)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine(" /// <inheritdoc/>");
|
|
writer.WriteLine(" public {0} {1}<{2}>({3})",
|
|
returnType, memberName, typeArg, parameterList);
|
|
if (!noConstraint)
|
|
{
|
|
writer.WriteLine(" where {0} : global::Opc.Ua.NodeState", typeArg);
|
|
}
|
|
writer.WriteLine(" => __inner.{0}<{1}>({2});",
|
|
memberName, typeArg, callArguments);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits one wrapper class for an <see cref="InstanceDesign"/>
|
|
/// describing a non-method instance. The class is rendered at the
|
|
/// indentation depth supplied by <paramref name="indent"/>; child
|
|
/// object/method wrappers are emitted recursively as nested types
|
|
/// one level deeper.
|
|
/// </summary>
|
|
private void EmitInstanceWrapper(
|
|
ITemplateWriter writer,
|
|
InstanceWrapper wrapper,
|
|
string indent)
|
|
{
|
|
string memberIndent = indent + Indent;
|
|
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}/// <summary>Typed wrapper for the predefined instance.</summary>", indent);
|
|
writer.WriteLine(
|
|
"{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{1}\", \"{2}\")]",
|
|
indent,
|
|
ToolName,
|
|
ToolVersion);
|
|
writer.WriteLine("{0}internal sealed class {1}", indent, wrapper.ClassName);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}private readonly global::Opc.Ua.Server.Fluent.INodeBuilder<{1}> __node;",
|
|
memberIndent, wrapper.NodeStateType);
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}internal {1}(global::Opc.Ua.Server.Fluent.INodeBuilder<{2}> node)",
|
|
memberIndent, wrapper.ClassName, wrapper.NodeStateType);
|
|
writer.WriteLine("{0}{{", memberIndent);
|
|
writer.WriteLine("{0}__node = node ?? throw new global::System.ArgumentNullException(nameof(node));",
|
|
memberIndent + Indent);
|
|
writer.WriteLine("{0}}}", memberIndent);
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}/// <summary>Underlying typed node builder.</summary>", memberIndent);
|
|
writer.WriteLine("{0}public global::Opc.Ua.Server.Fluent.INodeBuilder<{1}> Builder => __node;",
|
|
memberIndent, wrapper.NodeStateType);
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}/// <summary>Resolved underlying node.</summary>", memberIndent);
|
|
writer.WriteLine("{0}public {1} Node => __node.Node;", memberIndent, wrapper.NodeStateType);
|
|
|
|
foreach (ChildAccessor child in wrapper.Children!)
|
|
{
|
|
EmitChildAccessor(writer, child, memberIndent);
|
|
}
|
|
|
|
if (wrapper.SupportsPublish)
|
|
|
|
{
|
|
EmitPublishOverloads(writer, wrapper, memberIndent);
|
|
}
|
|
// Emit the nested method wrappers, then the nested object
|
|
// wrappers. Sibling order is leaf-name ordinal (set up by
|
|
// LinkChildWrappers) so generation is deterministic.
|
|
foreach (string methodKey in wrapper.ChildMethodKeys)
|
|
{
|
|
if (m_methodWrappers.TryGetValue(methodKey, out MethodWrapper? nestedMethod))
|
|
{
|
|
EmitMethodWrapper(writer, nestedMethod, memberIndent);
|
|
}
|
|
}
|
|
foreach (string childKey in wrapper.ChildObjectKeys)
|
|
{
|
|
if (m_wrappers.TryGetValue(childKey, out InstanceWrapper? nested))
|
|
{
|
|
EmitInstanceWrapper(writer, nested, memberIndent);
|
|
}
|
|
}
|
|
|
|
writer.WriteLine("{0}}}", indent);
|
|
}
|
|
|
|
// Per-ObjectType IComponentAccessor / IPropertyAccessor
|
|
/// <summary>
|
|
/// Emits one pair of static partial extension classes per
|
|
/// concrete (non-abstract) ObjectType in the model. The
|
|
/// extensions hang off
|
|
/// <c>Opc.Ua.IComponentAccessor<TState></c>
|
|
/// and <c>Opc.Ua.IPropertyAccessor<TState></c>
|
|
/// (Core-side markers, available to model-only assemblies)
|
|
/// so authors can walk a typed <c>INodeStateBuilder<TState></c>
|
|
/// tree via chained <c>.Components()</c> / <c>.Properties()</c>
|
|
/// pivots without spelling out browse-paths or QualifiedNames.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// Children are classified by their design-schema type:
|
|
/// </para>
|
|
/// <list type="bullet">
|
|
/// <item><description><see cref="PropertyDesign"/> →
|
|
/// <c>IPropertyAccessor<TState></c> extension returning
|
|
/// <c>IVariableBuilder<TValue></c>.</description></item>
|
|
/// <item><description><see cref="ObjectDesign"/>,
|
|
/// <see cref="MethodDesign"/>, and non-property
|
|
/// <see cref="VariableDesign"/> →
|
|
/// <c>IComponentAccessor<TState></c> extension returning
|
|
/// typed <c>INodeBuilder<TChildState></c>.</description></item>
|
|
/// </list>
|
|
/// <para>
|
|
/// Each accessor method uses an unqualified
|
|
/// <c>QualifiedName(name)</c> at the call site; the runtime
|
|
/// <c>NodeBuilder</c> cross-namespace fallback resolves the
|
|
/// child even when it carries a different namespace than the
|
|
/// parent (e.g. PumpType inheriting Operational from
|
|
/// MachineComponentType).
|
|
/// </para>
|
|
/// </remarks>
|
|
private void EmitObjectTypeAccessors(ITemplateWriter writer)
|
|
{
|
|
foreach (NodeDesign node in m_context.ModelDesign.GetNodeDesigns())
|
|
{
|
|
if (node is not ObjectTypeDesign objectType ||
|
|
m_context.ModelDesign.IsExcluded(objectType))
|
|
{
|
|
continue;
|
|
}
|
|
if (objectType.IsAbstract)
|
|
{
|
|
continue;
|
|
}
|
|
// Only emit accessors for types defined in this model —
|
|
// accessors for types declared in referenced models are
|
|
// emitted by those models' own generated source.
|
|
if (!string.Equals(objectType.SymbolicName?.Namespace,
|
|
m_context.ModelDesign.TargetNamespace?.Value,
|
|
StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
InstanceDesign[]? children = objectType.Children?.Items;
|
|
if (children == null || children.Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
string? parentClr = ResolveObjectTypeStateClr(objectType);
|
|
if (string.IsNullOrEmpty(parentClr))
|
|
{
|
|
continue;
|
|
}
|
|
string typeStem = objectType.SymbolicName?.Name
|
|
?? string.Empty;
|
|
if (typeStem.EndsWith("Type", StringComparison.Ordinal))
|
|
{
|
|
typeStem = typeStem[..^"Type".Length];
|
|
}
|
|
|
|
var components = new List<InstanceDesign>();
|
|
var properties = new List<InstanceDesign>();
|
|
foreach (InstanceDesign child in children)
|
|
{
|
|
if (child == null || m_context.ModelDesign.IsExcluded(child))
|
|
{
|
|
continue;
|
|
}
|
|
// Placeholders (<Name>) are never instantiated under that
|
|
// browse name, so an accessor walking to "<Name>" could
|
|
// never resolve a child.
|
|
if (child.ModellingRule is
|
|
ModellingRule.OptionalPlaceholder or
|
|
ModellingRule.MandatoryPlaceholder)
|
|
{
|
|
continue;
|
|
}
|
|
// No state class is emitted for an excluded type, so an
|
|
// accessor typed on it would not compile (CS0246).
|
|
if (child is ObjectDesign &&
|
|
child.TypeDefinitionNode is ObjectTypeDesign childType &&
|
|
m_context.ModelDesign.IsExcluded(childType))
|
|
{
|
|
continue;
|
|
}
|
|
if (child is PropertyDesign)
|
|
{
|
|
properties.Add(child);
|
|
}
|
|
else
|
|
{
|
|
components.Add(child);
|
|
}
|
|
}
|
|
|
|
if (components.Count > 0)
|
|
{
|
|
EmitTypeAccessorClass(
|
|
writer,
|
|
accessorKind: "Component",
|
|
accessorIface: "global::Opc.Ua.IComponentAccessor",
|
|
className: typeStem + "StateComponents",
|
|
parentClr: parentClr,
|
|
children: components);
|
|
}
|
|
if (properties.Count > 0)
|
|
{
|
|
EmitTypeAccessorClass(
|
|
writer,
|
|
accessorKind: "Property",
|
|
accessorIface: "global::Opc.Ua.IPropertyAccessor",
|
|
className: typeStem + "StateProperties",
|
|
parentClr: parentClr,
|
|
children: properties);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void EmitTypeAccessorClass(
|
|
ITemplateWriter writer,
|
|
string accessorKind,
|
|
string accessorIface,
|
|
string className,
|
|
string parentClr,
|
|
List<InstanceDesign> children)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
"/// <summary>Generator-emitted {0} accessor extensions for <c>{1}</c>.</summary>",
|
|
accessorKind, parentClr);
|
|
writer.WriteLine(
|
|
"[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{0}\", \"{1}\")]",
|
|
ToolName, ToolVersion);
|
|
writer.WriteLine(
|
|
"[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]");
|
|
writer.WriteLine("public static partial class {0}", className);
|
|
writer.WriteLine("{");
|
|
const string memberIndent = Indent;
|
|
foreach (InstanceDesign child in children
|
|
.OrderBy(c => c.SymbolicName?.Name, StringComparer.Ordinal))
|
|
{
|
|
EmitTypeAccessorMethod(
|
|
writer,
|
|
memberIndent,
|
|
accessorIface,
|
|
parentClr,
|
|
child);
|
|
}
|
|
writer.WriteLine("}");
|
|
}
|
|
|
|
private void EmitTypeAccessorMethod(
|
|
ITemplateWriter writer,
|
|
string indent,
|
|
string accessorIface,
|
|
string parentClr,
|
|
InstanceDesign child)
|
|
{
|
|
string browseName = GetBrowseName(child);
|
|
string accessorName = GetAccessorName(child);
|
|
if (child is PropertyDesign property)
|
|
{
|
|
string valueType = GetVariableValueClrType(property);
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
"{0}/// <summary>Walks the <c>{1}</c> HasProperty child.</summary>",
|
|
indent, EscapeXmlDoc(browseName));
|
|
writer.WriteLine(
|
|
"{0}public static global::Opc.Ua.Server.Fluent.IVariableBuilder<{1}> {2}(",
|
|
indent, valueType, accessorName);
|
|
writer.WriteLine(
|
|
"{0} this {1}<{2}> accessor)",
|
|
indent, accessorIface, parentClr);
|
|
writer.WriteLine(
|
|
"{0} => ((global::Opc.Ua.Server.Fluent.INodeBuilder<{1}>)accessor.Builder).Variable<{2}>(",
|
|
indent, parentClr, valueType);
|
|
writer.WriteLine(
|
|
"{0} new global::Opc.Ua.QualifiedName(\"{1}\"));",
|
|
indent, SourceGenerationUtils.Escape(browseName));
|
|
return;
|
|
}
|
|
if (child is VariableDesign variable)
|
|
{
|
|
string valueType = GetVariableValueClrType(variable);
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
"{0}/// <summary>Walks the <c>{1}</c> HasComponent variable child.</summary>",
|
|
indent, EscapeXmlDoc(browseName));
|
|
writer.WriteLine(
|
|
"{0}public static global::Opc.Ua.Server.Fluent.IVariableBuilder<{1}> {2}(",
|
|
indent, valueType, accessorName);
|
|
writer.WriteLine(
|
|
"{0} this {1}<{2}> accessor)",
|
|
indent, accessorIface, parentClr);
|
|
writer.WriteLine(
|
|
"{0} => ((global::Opc.Ua.Server.Fluent.INodeBuilder<{1}>)accessor.Builder).Variable<{2}>(",
|
|
indent, parentClr, valueType);
|
|
writer.WriteLine(
|
|
"{0} new global::Opc.Ua.QualifiedName(\"{1}\"));",
|
|
indent, SourceGenerationUtils.Escape(browseName));
|
|
return;
|
|
}
|
|
// ObjectDesign / MethodDesign — both are HasComponent.
|
|
string? childStateClr = ResolveChildStateClr(child);
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
"{0}/// <summary>Walks the <c>{1}</c> HasComponent child.</summary>",
|
|
indent, EscapeXmlDoc(browseName));
|
|
writer.WriteLine(
|
|
"{0}public static global::Opc.Ua.Server.Fluent.INodeBuilder<{1}> {2}(",
|
|
indent, childStateClr, accessorName);
|
|
writer.WriteLine(
|
|
"{0} this {1}<{2}> accessor)",
|
|
indent, accessorIface, parentClr);
|
|
writer.WriteLine(
|
|
"{0} => ((global::Opc.Ua.Server.Fluent.INodeBuilder<{1}>)accessor.Builder).Child<{2}>(",
|
|
indent, parentClr, childStateClr);
|
|
writer.WriteLine(
|
|
"{0} new global::Opc.Ua.QualifiedName(\"{1}\"));",
|
|
indent, SourceGenerationUtils.Escape(browseName));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the C# state class for an ObjectType design. Uses
|
|
/// the model's namespace prefix table to find the C# namespace
|
|
/// the type lives in; strips the conventional <c>Type</c>
|
|
/// suffix and appends <c>State</c>. No state class is emitted for
|
|
/// an excluded type, so it resolves to the state class of the
|
|
/// nearest emitted ancestor (the rule the proxy generator uses for
|
|
/// base types), falling back to <c>BaseObjectState</c>.
|
|
/// </summary>
|
|
private string? ResolveObjectTypeStateClr(ObjectTypeDesign? type)
|
|
{
|
|
while (type != null && m_context.ModelDesign.IsExcluded(type))
|
|
{
|
|
type = type.BaseTypeNode as ObjectTypeDesign;
|
|
if (type == null)
|
|
{
|
|
return "global::Opc.Ua.BaseObjectState";
|
|
}
|
|
}
|
|
string? typeName = type?.SymbolicName?.Name;
|
|
if (string.IsNullOrEmpty(typeName))
|
|
{
|
|
return null;
|
|
}
|
|
if (!string.IsNullOrEmpty(type!.ClassName))
|
|
{
|
|
return type.GetClassName(m_context.ModelDesign.Namespaces) + "State";
|
|
}
|
|
string className = typeName.EndsWith("Type", StringComparison.Ordinal)
|
|
? typeName[..^"Type".Length]
|
|
: typeName;
|
|
string stateName = className + "State";
|
|
string? nsUri = type.SymbolicName?.Namespace;
|
|
string prefix = ResolveCSharpNamespaceForUri(nsUri);
|
|
return string.IsNullOrEmpty(prefix)
|
|
? "global::Opc.Ua." + stateName
|
|
: "global::" + prefix + "." + stateName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves the C# state class for a HasComponent child by
|
|
/// looking at its TypeDefinition (the spec-declared type, not
|
|
/// the runtime state class). Falls back to the lowest-common-
|
|
/// denominator state when no TypeDefinition is declared.
|
|
/// </summary>
|
|
private string? ResolveChildStateClr(InstanceDesign child)
|
|
{
|
|
if (child is MethodDesign)
|
|
{
|
|
return "global::Opc.Ua.MethodState";
|
|
}
|
|
System.Xml.XmlQualifiedName? typeDef = child?.TypeDefinition;
|
|
if (typeDef == null || string.IsNullOrEmpty(typeDef.Name))
|
|
{
|
|
return ResolveFallbackStateClrType(child!);
|
|
}
|
|
if (child!.TypeDefinitionNode is ObjectTypeDesign objectType)
|
|
{
|
|
return ResolveObjectTypeStateClr(objectType);
|
|
}
|
|
string stateName = typeDef.Name.EndsWith("Type", StringComparison.Ordinal)
|
|
? typeDef.Name[..^"Type".Length] + "State"
|
|
: typeDef.Name + "State";
|
|
string prefix = ResolveCSharpNamespaceForUri(typeDef.Namespace);
|
|
return string.IsNullOrEmpty(prefix)
|
|
? "global::Opc.Ua." + stateName
|
|
: "global::" + prefix + "." + stateName;
|
|
}
|
|
|
|
private static string ResolveFallbackStateClrType(NodeDesign node)
|
|
{
|
|
if (node is ObjectDesign)
|
|
{
|
|
return "global::Opc.Ua.BaseObjectState";
|
|
}
|
|
if (node is MethodDesign)
|
|
{
|
|
return "global::Opc.Ua.MethodState";
|
|
}
|
|
if (node is VariableDesign)
|
|
{
|
|
return "global::Opc.Ua.BaseDataVariableState";
|
|
}
|
|
return "global::Opc.Ua.NodeState";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Maps a model namespace URI (e.g. <c>http://opcfoundation.org/UA/Pumps/</c>)
|
|
/// to the C# namespace prefix declared in the model's
|
|
/// Namespaces table (e.g. <c>Opc.Ua.Pumps</c>). Returns an
|
|
/// empty string when the URI is the standard OPC UA namespace
|
|
/// (whose C# prefix is the empty <c>Opc.Ua</c> root) or when
|
|
/// no mapping is found.
|
|
/// </summary>
|
|
private string ResolveCSharpNamespaceForUri(string? nsUri)
|
|
{
|
|
if (string.IsNullOrEmpty(nsUri))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
Namespace[] namespaces = m_context.ModelDesign.Namespaces;
|
|
if (namespaces != null)
|
|
{
|
|
for (int i = 0; i < namespaces.Length; i++)
|
|
{
|
|
Namespace ns = namespaces[i];
|
|
if (ns != null &&
|
|
string.Equals(ns.Value, nsUri,
|
|
StringComparison.Ordinal))
|
|
{
|
|
return ns.Prefix ?? string.Empty;
|
|
}
|
|
}
|
|
}
|
|
return string.Empty;
|
|
}
|
|
|
|
private void EmitChildAccessor(
|
|
ITemplateWriter writer,
|
|
ChildAccessor child,
|
|
string indent)
|
|
{
|
|
string bodyIndent = indent + Indent;
|
|
string innerIndent = bodyIndent + Indent;
|
|
|
|
writer.WriteLine();
|
|
switch (child.Kind)
|
|
{
|
|
case ChildKind.Variable:
|
|
writer.WriteLine("{0}/// <summary>Typed accessor for variable child <c>{1}</c>.</summary>",
|
|
indent, EscapeXmlDoc(child.BrowseName));
|
|
writer.WriteLine("{0}public global::Opc.Ua.Server.Fluent.IVariableBuilder<{1}> {2}",
|
|
indent, child.ValueClrType, child.AccessorName);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}get", bodyIndent);
|
|
writer.WriteLine("{0}{{", bodyIndent);
|
|
writer.WriteLine("{0}ushort __ns = __node.Builder.Context.NamespaceUris.GetIndexOrAppend(\"{1}\");",
|
|
innerIndent, SourceGenerationUtils.Escape(child.BrowseNamespaceUri));
|
|
writer.WriteLine("{0}return __node.Variable<{1}>(new global::Opc.Ua.QualifiedName(\"{2}\", __ns));",
|
|
innerIndent,
|
|
child.ValueClrType,
|
|
SourceGenerationUtils.Escape(child.BrowseName));
|
|
writer.WriteLine("{0}}}", bodyIndent);
|
|
writer.WriteLine("{0}}}", indent);
|
|
break;
|
|
case ChildKind.Method:
|
|
writer.WriteLine("{0}/// <summary>Typed accessor for method child <c>{1}</c>.</summary>",
|
|
indent, EscapeXmlDoc(child.BrowseName));
|
|
writer.WriteLine("{0}public {1} {2}", indent, child.WrapperClassName, child.AccessorName);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}get", bodyIndent);
|
|
writer.WriteLine("{0}{{", bodyIndent);
|
|
writer.WriteLine("{0}ushort __ns = __node.Builder.Context.NamespaceUris.GetIndexOrAppend(\"{1}\");",
|
|
innerIndent, SourceGenerationUtils.Escape(child.BrowseNamespaceUri));
|
|
writer.WriteLine("{0}return new {1}(__node.Child<global::Opc.Ua.MethodState>(new global::Opc.Ua.QualifiedName(\"{2}\", __ns)));",
|
|
innerIndent,
|
|
child.WrapperClassName,
|
|
SourceGenerationUtils.Escape(child.BrowseName));
|
|
writer.WriteLine("{0}}}", bodyIndent);
|
|
writer.WriteLine("{0}}}", indent);
|
|
break;
|
|
case ChildKind.Object:
|
|
writer.WriteLine("{0}/// <summary>Typed accessor for object child <c>{1}</c>.</summary>",
|
|
indent, EscapeXmlDoc(child.BrowseName));
|
|
writer.WriteLine("{0}public {1} {2}", indent, child.WrapperClassName, child.AccessorName);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}get", bodyIndent);
|
|
writer.WriteLine("{0}{{", bodyIndent);
|
|
writer.WriteLine("{0}ushort __ns = __node.Builder.Context.NamespaceUris.GetIndexOrAppend(\"{1}\");",
|
|
innerIndent, SourceGenerationUtils.Escape(child.BrowseNamespaceUri));
|
|
writer.WriteLine("{0}return new {1}(__node.Child<{2}>(new global::Opc.Ua.QualifiedName(\"{3}\", __ns)));",
|
|
innerIndent,
|
|
child.WrapperClassName,
|
|
child.ChildStateType,
|
|
SourceGenerationUtils.Escape(child.BrowseName));
|
|
writer.WriteLine("{0}}}", bodyIndent);
|
|
writer.WriteLine("{0}}}", indent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the typed <c>Publish<TEvent></c> overloads on a
|
|
/// notifier-capable wrapper. Both overloads forward to
|
|
/// <c>Opc.Ua.Server.Fluent.EventNotifierBuilderExtensions</c>;
|
|
/// the wrapper's underlying node-state type is bound as the
|
|
/// <c>TNotifier</c> type argument so callers don't need to spell
|
|
/// it out. The shape mirrors the extension's two overloads (direct
|
|
/// stream + factory).
|
|
/// </summary>
|
|
private static void EmitPublishOverloads(
|
|
ITemplateWriter writer,
|
|
InstanceWrapper wrapper,
|
|
string indent)
|
|
{
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
"{0}/// <summary>Registers an event source for this notifier; lazy by default. See <see cref=\"global::Opc.Ua.Server.Fluent.EventPublishOptions\"/> for activation tuning.</summary>",
|
|
indent);
|
|
writer.WriteLine(
|
|
"{0}public global::Opc.Ua.Server.Fluent.INodeBuilder<{1}> Publish<TEvent>(",
|
|
indent, wrapper.NodeStateType);
|
|
writer.WriteLine(
|
|
"{0}global::System.Collections.Generic.IAsyncEnumerable<TEvent> source,",
|
|
indent + Indent);
|
|
writer.WriteLine(
|
|
"{0}global::Opc.Ua.Server.Fluent.EventPublishOptions? options = null)",
|
|
indent + Indent);
|
|
writer.WriteLine(
|
|
"{0}where TEvent : global::Opc.Ua.BaseEventState",
|
|
indent + Indent);
|
|
writer.WriteLine(
|
|
"{0}=> global::Opc.Ua.Server.Fluent.EventNotifierBuilderExtensions.Publish<{1}, TEvent>(__node, source, options);",
|
|
indent + Indent, wrapper.NodeStateType);
|
|
|
|
writer.WriteLine();
|
|
writer.WriteLine(
|
|
"{0}/// <summary>Registers a factory-based event source for this notifier; the factory runs on each activation. See <see cref=\"global::Opc.Ua.Server.Fluent.EventPublishOptions\"/> for activation tuning.</summary>",
|
|
indent);
|
|
writer.WriteLine(
|
|
"{0}public global::Opc.Ua.Server.Fluent.INodeBuilder<{1}> Publish<TEvent>(",
|
|
indent, wrapper.NodeStateType);
|
|
writer.WriteLine(
|
|
"{0}global::System.Func<{1}, global::Opc.Ua.ISystemContext, global::System.Threading.CancellationToken, global::System.Collections.Generic.IAsyncEnumerable<TEvent>> factory,",
|
|
indent + Indent, wrapper.NodeStateType);
|
|
writer.WriteLine(
|
|
"{0}global::Opc.Ua.Server.Fluent.EventPublishOptions? options = null)",
|
|
indent + Indent);
|
|
writer.WriteLine(
|
|
"{0}where TEvent : global::Opc.Ua.BaseEventState",
|
|
indent + Indent);
|
|
writer.WriteLine(
|
|
"{0}=> global::Opc.Ua.Server.Fluent.EventNotifierBuilderExtensions.Publish<{1}, TEvent>(__node, factory, options);",
|
|
indent + Indent, wrapper.NodeStateType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits one wrapper class for a method instance with typed
|
|
/// <c>OnCall</c> overloads at the supplied <paramref name="indent"/>.
|
|
/// </summary>
|
|
private void EmitMethodWrapper(
|
|
ITemplateWriter writer,
|
|
MethodWrapper method,
|
|
string indent)
|
|
{
|
|
string memberIndent = indent + Indent;
|
|
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}/// <summary>Typed method-call wrapper for the predefined method.</summary>", indent);
|
|
writer.WriteLine(
|
|
"{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"{1}\", \"{2}\")]",
|
|
indent,
|
|
ToolName,
|
|
ToolVersion);
|
|
writer.WriteLine("{0}internal sealed class {1}", indent, method.ClassName);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}private readonly global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.MethodState> __node;",
|
|
memberIndent);
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}internal {1}(global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.MethodState> node)",
|
|
memberIndent, method.ClassName);
|
|
writer.WriteLine("{0}{{", memberIndent);
|
|
writer.WriteLine("{0}__node = node ?? throw new global::System.ArgumentNullException(nameof(node));",
|
|
memberIndent + Indent);
|
|
writer.WriteLine("{0}}}", memberIndent);
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}/// <summary>Underlying typed node builder. Use to drop into the non-typed fluent surface.</summary>",
|
|
memberIndent);
|
|
writer.WriteLine("{0}public global::Opc.Ua.Server.Fluent.INodeBuilder<global::Opc.Ua.MethodState> Builder => __node;",
|
|
memberIndent);
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}/// <summary>Resolved underlying method state.</summary>", memberIndent);
|
|
writer.WriteLine("{0}public global::Opc.Ua.MethodState Node => __node.Node;", memberIndent);
|
|
|
|
// Sync typed OnCall.
|
|
EmitMethodOnCall(writer, method, async: false, indent: memberIndent);
|
|
// Async typed OnCall.
|
|
EmitMethodOnCall(writer, method, async: true, indent: memberIndent);
|
|
|
|
writer.WriteLine("{0}}}", indent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits one OnCall overload for a method wrapper. The overload
|
|
/// shape is determined by the method's argument signature plus the
|
|
/// requested sync/async flavor. <paramref name="indent"/> is the
|
|
/// member indent of the enclosing method wrapper.
|
|
/// </summary>
|
|
private void EmitMethodOnCall(
|
|
ITemplateWriter writer,
|
|
MethodWrapper method,
|
|
bool async,
|
|
string indent)
|
|
{
|
|
string bodyIndent = indent + Indent;
|
|
string lambdaIndent = bodyIndent + Indent;
|
|
|
|
string targetNamespace = m_context.ModelDesign.TargetNamespace.Value;
|
|
Namespace[] namespaces = m_context.ModelDesign.Namespaces;
|
|
Parameter[]? inputs = method.Inputs;
|
|
Parameter[]? outputs = method.Outputs;
|
|
|
|
string returnTypeAnnotation = GetReturnTypeAnnotation(outputs!, targetNamespace, namespaces);
|
|
string handlerType;
|
|
if (async)
|
|
{
|
|
if (inputs!.Length == 0 && outputs!.Length == 0)
|
|
{
|
|
handlerType = "global::System.Func<global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask>";
|
|
}
|
|
else if (outputs!.Length == 0)
|
|
{
|
|
handlerType = CoreUtils.Format(
|
|
"global::System.Func<{0}, global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask>",
|
|
FormatInputTypeList(inputs, targetNamespace, namespaces));
|
|
}
|
|
else
|
|
{
|
|
handlerType = CoreUtils.Format(
|
|
"global::System.Func<{0}{1}global::System.Threading.CancellationToken, global::System.Threading.Tasks.ValueTask{2}>",
|
|
FormatInputTypeList(inputs, targetNamespace, namespaces),
|
|
inputs.Length == 0 ? string.Empty : ", ",
|
|
returnTypeAnnotation);
|
|
}
|
|
}
|
|
else if (inputs!.Length == 0 && outputs!.Length == 0)
|
|
{
|
|
handlerType = "global::System.Action";
|
|
}
|
|
else if (outputs!.Length == 0)
|
|
{
|
|
handlerType = CoreUtils.Format(
|
|
"global::System.Action<{0}>",
|
|
FormatInputTypeList(inputs, targetNamespace, namespaces));
|
|
}
|
|
else
|
|
{
|
|
handlerType = CoreUtils.Format(
|
|
"global::System.Func<{0}{1}{2}>",
|
|
FormatInputTypeList(inputs, targetNamespace, namespaces),
|
|
inputs.Length == 0 ? string.Empty : ", ",
|
|
StripAngleBrackets(returnTypeAnnotation, defaultIfEmpty: "void"));
|
|
}
|
|
|
|
writer.WriteLine();
|
|
writer.WriteLine("{0}/// <summary>Wires the method-call handler ({1}).</summary>",
|
|
indent, async ? "async" : "sync");
|
|
writer.WriteLine("{0}public {1} OnCall({2} handler)", indent, method.ClassName, handlerType);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}if (handler == null) throw new global::System.ArgumentNullException(nameof(handler));",
|
|
bodyIndent);
|
|
if (async)
|
|
{
|
|
writer.WriteLine("{0}__node.OnCall(async (", bodyIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.ISystemContext __ctx,", lambdaIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.MethodState __m,", lambdaIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.NodeId __oid,", lambdaIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.ArrayOf<global::Opc.Ua.Variant> __inputs,", lambdaIndent);
|
|
writer.WriteLine("{0}global::System.Collections.Generic.List<global::Opc.Ua.Variant> __outputs,", lambdaIndent);
|
|
writer.WriteLine("{0}global::System.Threading.CancellationToken __ct) =>", lambdaIndent);
|
|
writer.WriteLine("{0}{{", bodyIndent);
|
|
}
|
|
else
|
|
{
|
|
writer.WriteLine("{0}__node.OnCall((", bodyIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.ISystemContext __ctx,", lambdaIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.MethodState __m,", lambdaIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.NodeId __oid,", lambdaIndent);
|
|
writer.WriteLine("{0}global::Opc.Ua.ArrayOf<global::Opc.Ua.Variant> __inputs,", lambdaIndent);
|
|
writer.WriteLine("{0}global::System.Collections.Generic.List<global::Opc.Ua.Variant> __outputs) =>", lambdaIndent);
|
|
writer.WriteLine("{0}{{", bodyIndent);
|
|
}
|
|
|
|
// Validate input arg count.
|
|
if (inputs.Length > 0)
|
|
{
|
|
writer.WriteLine("{0}if (__inputs.Count < {1})", lambdaIndent, inputs.Length);
|
|
writer.WriteLine("{0}{{", lambdaIndent);
|
|
writer.WriteLine("{0}return new global::Opc.Ua.ServiceResult(global::Opc.Ua.StatusCodes.BadArgumentsMissing);",
|
|
lambdaIndent + Indent);
|
|
writer.WriteLine("{0}}}", lambdaIndent);
|
|
}
|
|
|
|
// Unpack inputs.
|
|
for (int ii = 0; ii < inputs.Length; ii++)
|
|
{
|
|
EmitInputUnpack(writer, inputs[ii], ii, targetNamespace, namespaces, lambdaIndent);
|
|
}
|
|
|
|
// Invoke user handler.
|
|
if (async)
|
|
{
|
|
if (outputs.Length == 0)
|
|
{
|
|
writer.Write("{0}await handler(", lambdaIndent);
|
|
EmitInputArgPassThrough(writer, inputs, withCt: true);
|
|
writer.WriteLine(").ConfigureAwait(false);");
|
|
}
|
|
else
|
|
{
|
|
writer.Write("{0}var __r = await handler(", lambdaIndent);
|
|
EmitInputArgPassThrough(writer, inputs, withCt: true);
|
|
writer.WriteLine(").ConfigureAwait(false);");
|
|
}
|
|
}
|
|
else if (outputs.Length == 0)
|
|
{
|
|
writer.Write("{0}handler(", lambdaIndent);
|
|
EmitInputArgPassThrough(writer, inputs, withCt: false);
|
|
writer.WriteLine(");");
|
|
}
|
|
else
|
|
{
|
|
writer.Write("{0}var __r = handler(", lambdaIndent);
|
|
EmitInputArgPassThrough(writer, inputs, withCt: false);
|
|
writer.WriteLine(");");
|
|
}
|
|
|
|
// Marshal outputs.
|
|
for (int ii = 0; ii < outputs.Length; ii++)
|
|
{
|
|
EmitOutputBox(writer, outputs[ii], ii, outputs.Length, lambdaIndent);
|
|
}
|
|
|
|
writer.WriteLine("{0}return global::Opc.Ua.ServiceResult.Good;", lambdaIndent);
|
|
writer.WriteLine("{0}}});", bodyIndent);
|
|
writer.WriteLine("{0}return this;", bodyIndent);
|
|
writer.WriteLine("{0}}}", indent);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the typed unpack code for a single input argument. Mirrors
|
|
/// the logic in <c>ObjectTypeProxyGenerator</c>. <paramref name="indent"/>
|
|
/// is the lambda-body indent of the surrounding OnCall.
|
|
/// </summary>
|
|
private static void EmitInputUnpack(
|
|
ITemplateWriter writer,
|
|
Parameter input,
|
|
int index,
|
|
string targetNamespace,
|
|
Namespace[] namespaces,
|
|
string indent)
|
|
{
|
|
string innerIndent = indent + Indent;
|
|
string typeName = input.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
input.ValueRank,
|
|
targetNamespace,
|
|
namespaces,
|
|
input.IsOptional);
|
|
string local = "__a" + index;
|
|
switch (input.DataTypeNode!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined:
|
|
writer.WriteLine("{0}if (!__inputs[{1}].TryGetStructure(out {2} {3}))",
|
|
indent, index, typeName, local);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}return new global::Opc.Ua.ServiceResult(global::Opc.Ua.StatusCodes.BadInvalidArgument);",
|
|
innerIndent);
|
|
writer.WriteLine("{0}}}", indent);
|
|
break;
|
|
case BasicDataType.BaseDataType when input.ValueRank == ValueRank.Scalar:
|
|
writer.WriteLine("{0}{1} {2} = __inputs[{3}];", indent, typeName, local, index);
|
|
break;
|
|
default:
|
|
writer.WriteLine("{0}if (!__inputs[{1}].TryGetValue(out {2} {3}))",
|
|
indent, index, typeName, local);
|
|
writer.WriteLine("{0}{{", indent);
|
|
writer.WriteLine("{0}return new global::Opc.Ua.ServiceResult(global::Opc.Ua.StatusCodes.BadInvalidArgument);",
|
|
innerIndent);
|
|
writer.WriteLine("{0}}}", indent);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the comma-separated list of unpacked input locals (plus
|
|
/// optional CancellationToken) for the user-handler invocation.
|
|
/// </summary>
|
|
private static void EmitInputArgPassThrough(
|
|
ITemplateWriter writer,
|
|
Parameter[] inputs,
|
|
bool withCt)
|
|
{
|
|
for (int ii = 0; ii < inputs.Length; ii++)
|
|
{
|
|
if (ii > 0)
|
|
{
|
|
writer.Write(", ");
|
|
}
|
|
writer.Write("__a" + ii);
|
|
}
|
|
if (withCt)
|
|
{
|
|
if (inputs.Length > 0)
|
|
{
|
|
writer.Write(", ");
|
|
}
|
|
writer.Write("__ct");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the boxing code for a single output argument. For multi-
|
|
/// output methods the user returns a <c>ValueTuple</c> and we
|
|
/// destructure by field name (<c>Item1</c>, <c>Item2</c>, …).
|
|
/// <paramref name="indent"/> is the lambda-body indent of the
|
|
/// surrounding OnCall.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The base <see cref="MethodState"/> dispatcher
|
|
/// pre-populates the outputs list with one default-valued
|
|
/// <see cref="Variant"/> per declared output argument
|
|
/// before invoking the user handler, so the wrapper assigns
|
|
/// boxed values by index rather than appending to avoid
|
|
/// double-counting outputs at the wire.
|
|
/// </remarks>
|
|
private static void EmitOutputBox(
|
|
ITemplateWriter writer,
|
|
Parameter output,
|
|
int index,
|
|
int totalOutputs,
|
|
string indent)
|
|
{
|
|
string source;
|
|
if (totalOutputs == 1)
|
|
{
|
|
source = "__r";
|
|
}
|
|
else
|
|
{
|
|
source = "__r.Item" + (index + 1).ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
string indexLiteral = index.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
|
|
|
switch (output.DataTypeNode!.BasicDataType)
|
|
{
|
|
case BasicDataType.UserDefined:
|
|
writer.WriteLine("{0}__outputs[{1}] = global::Opc.Ua.Variant.FromStructure({2});", indent, indexLiteral, source);
|
|
break;
|
|
case BasicDataType.BaseDataType when output.ValueRank == ValueRank.Scalar:
|
|
writer.WriteLine("{0}__outputs[{1}] = {2};", indent, indexLiteral, source);
|
|
break;
|
|
default:
|
|
writer.WriteLine("{0}__outputs[{1}] = global::Opc.Ua.Variant.From({2});", indent, indexLiteral, source);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the <c>ValueTask<…></c> suffix used in the method's
|
|
/// async return type. Mirrors
|
|
/// <see cref="ObjectTypeProxyGenerator"/> verbatim.
|
|
/// </summary>
|
|
private static string GetReturnTypeAnnotation(
|
|
Parameter[] outputs,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
if (outputs.Length == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
if (outputs.Length == 1)
|
|
{
|
|
return CoreUtils.Format(
|
|
"<{0}>",
|
|
outputs[0].DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
outputs[0].ValueRank,
|
|
targetNamespace,
|
|
namespaces,
|
|
outputs[0].IsOptional));
|
|
}
|
|
var sb = new System.Text.StringBuilder();
|
|
sb.Append("<(");
|
|
for (int ii = 0; ii < outputs.Length; ii++)
|
|
{
|
|
if (ii > 0)
|
|
{
|
|
sb.Append(", ");
|
|
}
|
|
sb.Append(outputs[ii].DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
outputs[ii].ValueRank,
|
|
targetNamespace,
|
|
namespaces,
|
|
outputs[ii].IsOptional))
|
|
.Append(" Item" + (ii + 1).ToString(System.Globalization.CultureInfo.InvariantCulture));
|
|
}
|
|
sb.Append(")>");
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string StripAngleBrackets(string returnTypeAnnotation, string defaultIfEmpty)
|
|
{
|
|
if (string.IsNullOrEmpty(returnTypeAnnotation))
|
|
{
|
|
return defaultIfEmpty;
|
|
}
|
|
// Strip leading '<' and trailing '>'.
|
|
return returnTypeAnnotation[1..^1];
|
|
}
|
|
|
|
private static string FormatInputTypeList(
|
|
Parameter[] inputs,
|
|
string targetNamespace,
|
|
Namespace[] namespaces)
|
|
{
|
|
var sb = new System.Text.StringBuilder();
|
|
for (int ii = 0; ii < inputs.Length; ii++)
|
|
{
|
|
if (ii > 0)
|
|
{
|
|
sb.Append(", ");
|
|
}
|
|
sb.Append(inputs[ii].DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
inputs[ii].ValueRank,
|
|
targetNamespace,
|
|
namespaces,
|
|
inputs[ii].IsOptional));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
// Helpers
|
|
/// <summary>
|
|
/// Returns the wrapper-key string used to deduplicate wrappers and
|
|
/// to compose CLR class names. Combines the root's symbolic id
|
|
/// with the relative path from the root.
|
|
/// </summary>
|
|
private static string ComposeKey(InstanceDesign root, string? relativePath)
|
|
{
|
|
string rootId = root?.SymbolicId?.Name ?? string.Empty;
|
|
if (string.IsNullOrEmpty(relativePath))
|
|
{
|
|
return rootId;
|
|
}
|
|
return rootId + "_" + relativePath;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the simple leaf name (the last segment of the relative
|
|
/// path) used as the C# class name's stem. Honors the convention
|
|
/// that segment names themselves can contain underscores so we
|
|
/// rely on the instance's <c>SymbolicName.Name</c> rather than
|
|
/// splitting on <c>NodeDesign.PathChar</c>. For the root
|
|
/// instance (empty <paramref name="relativePath"/>) the leaf is
|
|
/// the root's own <c>SymbolicId.Name</c>.
|
|
/// </summary>
|
|
private static string ResolveLeafName(
|
|
InstanceDesign root,
|
|
string? relativePath,
|
|
NodeDesign? instance)
|
|
{
|
|
if (string.IsNullOrEmpty(relativePath))
|
|
{
|
|
return root?.SymbolicId?.Name ?? string.Empty;
|
|
}
|
|
string? symbolicName = instance?.SymbolicName?.Name;
|
|
if (string.IsNullOrEmpty(symbolicName))
|
|
{
|
|
return relativePath;
|
|
}
|
|
return symbolicName;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the wrapper key of the lexical parent for the wrapper
|
|
/// at <paramref name="relativePath"/> under <paramref name="root"/>.
|
|
/// Returns <c>null</c> for the root itself (lives at namespace
|
|
/// scope), the root's key for direct children, and the parent
|
|
/// path's key for deeper nesting.
|
|
/// </summary>
|
|
private static string? ResolveParentKey(
|
|
InstanceDesign root,
|
|
string relativePath,
|
|
string leafName)
|
|
{
|
|
if (string.IsNullOrEmpty(relativePath))
|
|
{
|
|
return null;
|
|
}
|
|
if (relativePath.Length == leafName.Length)
|
|
{
|
|
return ComposeKey(root, string.Empty);
|
|
}
|
|
int trim = leafName.Length + 1;
|
|
if (relativePath.Length <= trim)
|
|
{
|
|
return ComposeKey(root, string.Empty);
|
|
}
|
|
return ComposeKey(root, relativePath[..^trim]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the wrapper's CLR class name. Wrappers are emitted as
|
|
/// nested types so the simple leaf name is sufficient — full
|
|
/// dotted access is composed by the consumer through the chain
|
|
/// of typed accessor properties.
|
|
/// </summary>
|
|
private static string ComposeWrapperClassName(string leafName, string suffix)
|
|
{
|
|
return (leafName ?? string.Empty) + suffix;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Members every generated instance wrapper declares itself, which a child
|
|
/// accessor therefore cannot be named after.
|
|
/// </summary>
|
|
private static readonly HashSet<string> s_reservedWrapperMembers =
|
|
new(StringComparer.Ordinal) { "Builder", "Node" };
|
|
|
|
/// <summary>
|
|
/// Members the generated typed manager builder forwards from
|
|
/// <c>INodeManagerBuilder</c> (see <see cref="EmitTypedManagerImpl"/>),
|
|
/// which a top-level accessor therefore cannot be named after.
|
|
/// </summary>
|
|
private static readonly HashSet<string> s_typedBuilderMembers =
|
|
new(StringComparer.Ordinal)
|
|
{
|
|
"Context",
|
|
"NodeManager",
|
|
"Dispatcher",
|
|
"DefaultNamespaceIndex",
|
|
"Import",
|
|
"Node",
|
|
"NodeFromTypeId",
|
|
"Variable",
|
|
"VariableFromTypeId",
|
|
"VariableFromDataTypeId",
|
|
"Add",
|
|
"AddRoot",
|
|
"TryGetNode",
|
|
"AddFolder",
|
|
"AddObject",
|
|
"AddVariable",
|
|
"AddMethod"
|
|
};
|
|
|
|
private static string GetAccessorName(NodeDesign node)
|
|
{
|
|
string? name = node?.SymbolicName?.Name;
|
|
if (string.IsNullOrEmpty(name))
|
|
{
|
|
return "Item";
|
|
}
|
|
return name.ToSafeSymbolName(toLowerCamelCase: false);
|
|
}
|
|
|
|
private static string GetBrowseName(NodeDesign node)
|
|
{
|
|
// Per the design schema the BrowseName is identical to the
|
|
// SymbolicName when it isn't explicitly overridden. Use the
|
|
// BrowseName when present so source-generated identifiers and
|
|
// the on-the-wire OPC UA browse name stay aligned.
|
|
if (!string.IsNullOrEmpty(node?.BrowseName))
|
|
{
|
|
return node.BrowseName;
|
|
}
|
|
return node?.SymbolicName?.Name ?? string.Empty;
|
|
}
|
|
|
|
private string ResolveNodeBrowseNamespace(NodeDesign? node)
|
|
{
|
|
string? ns = node?.SymbolicName?.Namespace;
|
|
if (!string.IsNullOrEmpty(ns))
|
|
{
|
|
return ns;
|
|
}
|
|
return m_context.ModelDesign.TargetNamespace?.Value ?? string.Empty;
|
|
}
|
|
|
|
private string ResolveNodeIdNamespace(NodeDesign node)
|
|
{
|
|
string? ns = node?.SymbolicId?.Namespace;
|
|
if (!string.IsNullOrEmpty(ns))
|
|
{
|
|
return ns;
|
|
}
|
|
return m_context.ModelDesign.TargetNamespace?.Value ?? string.Empty;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true when the supplied node should expose typed
|
|
/// <c>Publish<TEvent></c> overloads on its wrapper. A node
|
|
/// qualifies if it carries the <c>EventNotifier=SubscribeToEvents</c>
|
|
/// attribute (modeled as <see cref="ObjectDesign.SupportsEvents"/>;
|
|
/// the model validator auto-promotes nodes with forward
|
|
/// <c>HasEventSource</c>/<c>HasNotifier</c> references) or has a
|
|
/// forward <c>GeneratesEvent</c>/<c>AlwaysGeneratesEvent</c>
|
|
/// reference. Per the locked decision the typed overload is
|
|
/// emitted only on spec-accurate notifier candidates so call sites
|
|
/// don't drift from the model intent.
|
|
/// </summary>
|
|
private static bool QualifiesAsEventNotifier(NodeDesign? node)
|
|
{
|
|
if (node is ObjectDesign od && od.SupportsEvents)
|
|
{
|
|
return true;
|
|
}
|
|
Reference[]? references = node?.References;
|
|
if (references == null || references.Length == 0)
|
|
{
|
|
return false;
|
|
}
|
|
foreach (Reference reference in references)
|
|
{
|
|
if (reference == null || reference.IsInverse)
|
|
{
|
|
continue;
|
|
}
|
|
if (reference.ReferenceType?.Name is "GeneratesEvent" or
|
|
"AlwaysGeneratesEvent")
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the C# type name of the runtime <see cref="NodeState"/>
|
|
/// derivative for the supplied instance.
|
|
/// </summary>
|
|
private string? ResolveStateClrType(NodeDesign? node)
|
|
{
|
|
if (node is ObjectDesign objectDesign)
|
|
{
|
|
return ResolveChildStateClr(objectDesign);
|
|
}
|
|
return ResolveFallbackStateClrType(node!);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the CLR type name for a variable's value attribute,
|
|
/// inferred from the variable's <c>DataType</c> and <c>ValueRank</c>.
|
|
/// </summary>
|
|
private string GetVariableValueClrType(VariableDesign variable)
|
|
{
|
|
return variable.DataTypeNode!.GetMethodArgumentTypeAsCode(
|
|
variable.ValueRank,
|
|
m_context.ModelDesign.TargetNamespace.Value,
|
|
m_context.ModelDesign.Namespaces,
|
|
isOptional: false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Emits the constructor argument used to materialize the
|
|
/// top-level instance's <see cref="NodeId"/>. Numeric ids preferred
|
|
/// when present, then string, Guid and Opaque ids; otherwise falls back
|
|
/// to the SymbolicId string.
|
|
/// </summary>
|
|
private static string? EmitNodeIdConstructorArg(InstanceDesign node)
|
|
{
|
|
if (node.NumericIdSpecified && node.NumericId != 0u)
|
|
{
|
|
return CoreUtils.Format("{0}u",
|
|
node.NumericId.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
|
}
|
|
if (!string.IsNullOrEmpty(node.StringId))
|
|
{
|
|
return CoreUtils.Format("\"{0}\"", SourceGenerationUtils.Escape(node.StringId));
|
|
}
|
|
if (node.HasNonConstantIdentifier())
|
|
{
|
|
return ModelDesignExtensions.GetIdentifierAsCode(node.GetIdentifier()!, out _);
|
|
}
|
|
// No id assigned — fall back to the SymbolicId.Name as a string id.
|
|
return CoreUtils.Format("\"{0}\"",
|
|
SourceGenerationUtils.Escape(node.SymbolicId?.Name ?? string.Empty));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Escapes a value for safe inclusion in a generated XML documentation
|
|
/// comment. OptionalPlaceholder browse names such as <c><Binding></c>
|
|
/// contain angle brackets that would otherwise produce malformed XML
|
|
/// (CS1570) when the consuming project enables documentation-file
|
|
/// generation.
|
|
/// </summary>
|
|
private static string EscapeXmlDoc(string? value)
|
|
{
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
return value
|
|
.Replace("&", "&", StringComparison.Ordinal)
|
|
.Replace("<", "<", StringComparison.Ordinal)
|
|
.Replace(">", ">", StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// State
|
|
/// </summary>
|
|
private readonly IGeneratorContext m_context;
|
|
private Dictionary<string, InstanceWrapper> m_wrappers = [];
|
|
private Dictionary<string, MethodWrapper> m_methodWrappers = [];
|
|
|
|
/// <summary>
|
|
/// Single nesting step. Wrappers are emitted inside the body of
|
|
/// the file template at column 4; each additional nesting level
|
|
/// adds one Indent.
|
|
/// </summary>
|
|
private const string Indent = " ";
|
|
|
|
private static string? ToolName
|
|
=> System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
|
|
|
|
private static string ToolVersion
|
|
=> System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString();
|
|
|
|
private enum ChildKind
|
|
{
|
|
Variable,
|
|
Method,
|
|
Object
|
|
}
|
|
|
|
private sealed class InstanceWrapper
|
|
{
|
|
public string? Key;
|
|
public string? ClassName;
|
|
public string? LeafName;
|
|
public string? ParentKey;
|
|
public string? NodeStateType;
|
|
public string? BrowseNamespaceUri;
|
|
public bool SupportsPublish;
|
|
public List<ChildAccessor>? Children;
|
|
public List<string> ChildObjectKeys = [];
|
|
public List<string> ChildMethodKeys = [];
|
|
}
|
|
|
|
private sealed class ChildAccessor
|
|
{
|
|
public string? AccessorName;
|
|
public string? BrowseName;
|
|
public string? BrowseNamespaceUri;
|
|
public ChildKind Kind;
|
|
|
|
/// <summary>
|
|
/// CLR type name of the variable's value.
|
|
/// </summary>
|
|
public string? ValueClrType;
|
|
|
|
/// <summary>
|
|
/// Generated wrapper class name for a method or object child.
|
|
/// </summary>
|
|
public string? WrapperClassName;
|
|
|
|
/// <summary>
|
|
/// Key into <c>m_wrappers</c> for object children.
|
|
/// </summary>
|
|
public string? ChildKey;
|
|
|
|
/// <summary>
|
|
/// Node state type for object children.
|
|
/// </summary>
|
|
public string? ChildStateType;
|
|
}
|
|
|
|
private sealed class MethodWrapper
|
|
{
|
|
public string? Key;
|
|
public string? ClassName;
|
|
public string? LeafName;
|
|
public string? ParentKey;
|
|
public Parameter[]? Inputs;
|
|
public Parameter[]? Outputs;
|
|
}
|
|
}
|
|
}
|