/* ======================================================================== * 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.Collections.Immutable; using System.Linq; using Opc.Ua.Schema.Model; namespace Opc.Ua.SourceGeneration { /// /// Source Generation API /// public static class Generators { /// /// Generate code from design files /// /// Design files to process /// File system abstraction to use /// Output folder or null /// Telemetry context for logging /// Generator options /// allow subtypes /// Any additional csv files /// Models supplied by referenced /// assemblies (keyed by model URI). Used to seed the assembly /// dependency closure and may be empty. /// /// Optional [NodeManager] attribute bindings discovered in /// the consuming compilation. When supplied, each binding is /// matched to a design by /// (preferred) or, if no URI is given, by single-design fallback. /// Matched bindings force GenerateNodeManager = true for the /// design and override the manager class name and namespace. /// /// /// Optional callback invoked for each binding-related warning or /// error (e.g. unmatched URI, ambiguous fallback). Implementations /// typically convert these into Roslyn diagnostics. /// public static void GenerateCode( this DesignFileCollection designFiles, IFileSystem fileSystem, string outputDir, ITelemetryContext telemetry, GeneratorOptions options = null, bool useAllowSubtypes = false, List identifierFiles = null, IReadOnlyDictionary referencedModels = null, IReadOnlyList nodeManagerBindings = null, Action reportBindingDiagnostic = null) { if (designFiles.Targets == null || designFiles.Targets.Count == 0) { return; } options ??= new GeneratorOptions(); referencedModels ??= ImmutableDictionary.Empty; // Combine with embedded resources in this assembly. fileSystem = typeof(Generators).Assembly .AsFileSystem("Opc.Ua.SourceGeneration.Design") .WithFallback(fileSystem); HashSet usedBindings = nodeManagerBindings is { Count: > 0 } ? [] : null; int totalDesigns = designFiles.Targets.Count; foreach (DesignFileCollection model in designFiles.Group(identifierFiles)) { IModelDesign modelDesign = fileSystem.OpenModelDesign( model, options.Exclusions, telemetry, useAllowSubtypes); // Override resolution: if a referenced assembly already // provides this model under the same C# prefix, silently // skip local generation to avoid duplicate type emission. Namespace target = modelDesign.TargetNamespace; if (target != null && !string.IsNullOrEmpty(target.Value) && referencedModels.TryGetValue(target.Value, out ModelDependencyReference referenced) && string.Equals(referenced.Prefix, target.Prefix, StringComparison.Ordinal)) { continue; } DesignFileOptions effectiveOptions = ApplyNodeManagerBinding( model, modelDesign, nodeManagerBindings, usedBindings, totalDesigns, reportBindingDiagnostic); Generate(new GeneratorContext { FileSystem = fileSystem, OutputFolder = outputDir, ModelDesign = modelDesign, Telemetry = telemetry, Options = options, ReferencedModels = referencedModels }, validateSchemas: false, designOptions: effectiveOptions); } if (usedBindings != null && nodeManagerBindings != null && reportBindingDiagnostic != null) { foreach (NodeManagerAttributeBinding binding in nodeManagerBindings) { if (!usedBindings.Contains(binding)) { string selector = !string.IsNullOrEmpty(binding.NamespaceUri) ? "NamespaceUri='" + binding.NamespaceUri + "'" : !string.IsNullOrEmpty(binding.Design) ? "Design='" + binding.Design + "'" : "(no selector)"; reportBindingDiagnostic( binding, "[NodeManager] on '" + binding.TargetNamespace + "." + binding.TargetClassName + "' did not match any model design (" + selector + ")."); } } } } /// /// Resolve the effective per-design options by overlaying any /// matching [NodeManager] attribute binding on top of the /// existing . /// private static DesignFileOptions ApplyNodeManagerBinding( DesignFileCollection model, IModelDesign modelDesign, IReadOnlyList bindings, HashSet usedBindings, int totalDesigns, Action reportBindingDiagnostic) { DesignFileOptions effective = model.Options; if (bindings == null || bindings.Count == 0) { return effective; } string uri = modelDesign?.TargetNamespace?.Value; string designName = model.Targets.Count == 1 ? System.IO.Path.GetFileNameWithoutExtension(model.Targets[0]) : null; NodeManagerAttributeBinding match = null; // 1) exact URI match if (!string.IsNullOrEmpty(uri)) { match = bindings.FirstOrDefault(b => string.Equals(b.NamespaceUri, uri, StringComparison.Ordinal)); } // 2) design file name match if (match == null && !string.IsNullOrEmpty(designName)) { match = bindings.FirstOrDefault(b => !string.IsNullOrEmpty(b.Design) && string.Equals(b.Design, designName, StringComparison.OrdinalIgnoreCase)); } // 3) single-design / single-binding fallback if (match == null && totalDesigns == 1 && bindings.Count == 1 && string.IsNullOrEmpty(bindings[0].NamespaceUri) && string.IsNullOrEmpty(bindings[0].Design)) { match = bindings[0]; } if (match == null) { return effective; } // Detect ambiguity: multiple designs but binding has no selector. if (totalDesigns > 1 && string.IsNullOrEmpty(match.NamespaceUri) && string.IsNullOrEmpty(match.Design) && reportBindingDiagnostic != null) { reportBindingDiagnostic( match, "[NodeManager] on '" + match.TargetNamespace + "." + match.TargetClassName + "' has no NamespaceUri/Design selector but the project " + "contains multiple designs. Specify NamespaceUri to " + "disambiguate."); return effective; } usedBindings?.Add(match); return (effective ?? new DesignFileOptions()) with { GenerateNodeManager = true, NodeManagerNamespace = match.TargetNamespace, NodeManagerClassName = match.TargetClassName, EmitNodeManagerFactory = match.GenerateFactory }; } /// /// Generate from nodesets /// /// Nodesets to process /// File system abstraction to use /// Output folder or null /// Telemetry context for logging /// Generator options /// allow subtypes /// Models supplied by referenced /// assemblies (keyed by model URI). When a target's model URI /// is in this map the nodeset is skipped (referenced assembly /// already supplies the types). Transitive nodeset dependencies /// found in the map are also satisfied without erroring. /// /// Optional [NodeManager] attribute bindings discovered in /// the consuming compilation. When supplied, each binding is /// matched to a nodeset model by /// (preferred) /// or, if no URI is given, by single-design fallback. Matched /// bindings force GenerateNodeManager = true for the model /// and override the manager class name and namespace. /// /// /// Optional callback invoked for each binding-related warning or /// error (e.g. unmatched URI, ambiguous fallback). /// public static void GenerateCode( this NodesetFileCollection nodesets, IFileSystem fileSystem, string outputDir, ITelemetryContext telemetry, GeneratorOptions options = null, bool useAllowSubtypes = false, IReadOnlyDictionary referencedModels = null, IReadOnlyList nodeManagerBindings = null, Action reportBindingDiagnostic = null) { if (nodesets.Files.Count == 0) { return; } options ??= new GeneratorOptions(); referencedModels ??= ImmutableDictionary.Empty; // Combine with embedded resources in this assembly. fileSystem = typeof(Generators).Assembly .AsFileSystem("Opc.Ua.SourceGeneration.Design") .WithFallback(fileSystem); HashSet usedBindings = nodeManagerBindings is { Count: > 0 } ? [] : null; int totalDesigns = nodesets.ModelUris.Count(); foreach (string modelUri in nodesets.ModelUris) { List designFilesForModel = nodesets.GetDesignFileListForModel( modelUri, out NodesetFile nodeset, referencedModels); if (designFilesForModel == null || nodeset.Info.Ignore) { continue; } // Override resolution: if a referenced assembly already // provides this model under the same C# prefix, silently // skip local generation to avoid duplicate type emission. if (referencedModels.TryGetValue(modelUri, out ModelDependencyReference referenced) && string.Equals(referenced.Prefix, nodeset.Info.Prefix, StringComparison.Ordinal)) { continue; } // The rest of the input is processed as design files var model = new DesignFileCollection { Targets = designFilesForModel }; IModelDesign modelDesign = fileSystem.OpenModelDesign( model, options.Exclusions, telemetry, useAllowSubtypes); DesignFileOptions effectiveOptions = ApplyNodeManagerBinding( model, modelDesign, nodeManagerBindings, usedBindings, totalDesigns, reportBindingDiagnostic); Generate(new GeneratorContext { FileSystem = fileSystem, OutputFolder = outputDir, ModelDesign = modelDesign, Telemetry = telemetry, Options = options, ReferencedModels = referencedModels }, validateSchemas: false, designOptions: effectiveOptions); // TODO { // TODO AvailableNodeSets = nodesets.Files // TODO }; } if (usedBindings != null && nodeManagerBindings != null && reportBindingDiagnostic != null) { foreach (NodeManagerAttributeBinding binding in nodeManagerBindings) { if (!usedBindings.Contains(binding)) { string selector = !string.IsNullOrEmpty(binding.NamespaceUri) ? "NamespaceUri='" + binding.NamespaceUri + "'" : !string.IsNullOrEmpty(binding.Design) ? "Design='" + binding.Design + "'" : "(no selector)"; reportBindingDiagnostic( binding, "[NodeManager] on '" + binding.TargetNamespace + "." + binding.TargetClassName + "' did not match any model design (" + selector + ")."); } } } } /// /// Generate the .net stack code /// /// Generator type /// The root file system to use /// Output folder or null /// A telemetry context for logging /// Generator options public static void GenerateStack( StackGenerationType generatorType, IFileSystem fileSystem, string outputDir, ITelemetryContext telemetry, GeneratorOptions options = null) { options ??= new GeneratorOptions(); // Combine with embedded resources in this assembly. fileSystem = typeof(Generators).Assembly .AsFileSystem("Opc.Ua.SourceGeneration.Design") .WithFallback(fileSystem); IModelDesign modelDesign = fileSystem.OpenModelDesign( new DesignFileCollection { Targets = [ BuiltInDesignFiles.StandardTypesXml, BuiltInDesignFiles.UACoreServicesXml ], IdentifierFilePath = BuiltInDesignFiles.StandardTypesCsv, Options = new DesignFileOptions { StartId = 0, ModelVersion = "1.05.06", ModelPublicationDate = "2025-11-08", ReleaseCandidate = true } }, options.Exclusions, telemetry, false); var generatorContext = new GeneratorContext { FileSystem = fileSystem, OutputFolder = outputDir, ModelDesign = modelDesign, Telemetry = telemetry, Options = options }; if ((generatorType & StackGenerationType.Stack) != 0) { var clientApiGenerator = new ClientApiGenerator(generatorContext); clientApiGenerator.Emit(); var serverApiGenerator = new ServerApiGenerator(generatorContext); serverApiGenerator.Emit(); var endpointsGenerator = new EndpointsGenerator(generatorContext); endpointsGenerator.Emit(); // Emit ObjectType client proxies for every standard UA // ObjectType so downstream model proxies (e.g. GDS) can // derive from them. Proxies are emitted into the model's // own namespace (Opc.Ua for the standard NodeSet) — no // namespace override. Suppressed when the consumer opts // out via OmitObjectTypeProxies. if (!options.OmitObjectTypeProxies) { var stackProxyContext = new GeneratorContext { FileSystem = generatorContext.FileSystem, OutputFolder = generatorContext.OutputFolder, ModelDesign = generatorContext.ModelDesign, Telemetry = generatorContext.Telemetry, Options = new GeneratorOptions { OptimizeForCompileSpeed = options.OptimizeForCompileSpeed, Exclusions = options.Exclusions, Cancellation = options.Cancellation, UseUtf8StringLiterals = options.UseUtf8StringLiterals } }; var stackProxyGenerator = new ObjectTypeProxyGenerator(stackProxyContext); stackProxyGenerator.Emit(); } // Emit event-record records for every standard UA // event type. Records reference EventRecord (in // Opc.Ua.Core) so they are only emitted in the Stack // path (which runs against Opc.Ua.Core), not in the // Models path (which runs against Opc.Ua.Core.Types). var stackRecordContext = new GeneratorContext { FileSystem = generatorContext.FileSystem, OutputFolder = generatorContext.OutputFolder, ModelDesign = generatorContext.ModelDesign, Telemetry = generatorContext.Telemetry, Options = new GeneratorOptions { OptimizeForCompileSpeed = options.OptimizeForCompileSpeed, Exclusions = options.Exclusions, Cancellation = options.Cancellation, UseUtf8StringLiterals = options.UseUtf8StringLiterals } }; var stackRecordGenerator = new EventRecordGenerator(stackRecordContext); stackRecordGenerator.Emit(); } if ((generatorType & StackGenerationType.Models) != 0) { var attributesGenerator = new AttributesGenerator(generatorContext); attributesGenerator.Emit(); var statusCodesGenerator = new StatusCodesGenerator(generatorContext); statusCodesGenerator.Emit(); var serverCapabilitiesGenerator = new ServerCapabilitiesGenerator(generatorContext); serverCapabilitiesGenerator.Emit(); Generate(generatorContext, !options.OptimizeForCompileSpeed); } } /// /// Generates all files /// private static void Generate( GeneratorContext context, bool validateSchemas = false, DesignFileOptions designOptions = null) { // Generate schemas var xmlSchemaGenerator = new XmlSchemaGenerator(context) { ValidateOutput = validateSchemas }; IEnumerable xmlSchemaResource = xmlSchemaGenerator.Emit(); var binarySchemaGenerator = new BinarySchemaGenerator(context) { ValidateOutput = validateSchemas }; IEnumerable binarySchemaResource = binarySchemaGenerator.Emit(); var schemaResources = new ResourceGenerator(context); schemaResources.Embed( context.ModelDesign.TargetNamespace.Prefix, "XmlSchemas", false, [.. binarySchemaResource, .. xmlSchemaResource]); // Must run after schema generation to initilize the dictionaries. var constantsGenerator = new ConstantsGenerator(context); constantsGenerator.Emit(); var nodeIdGenerator = new NodeIdGenerator(context); nodeIdGenerator.Emit(); var nodeStateCodeGenerator = new NodeStateGenerator(context); nodeStateCodeGenerator.Emit(); var dataTypesGenerator = new DataTypeGenerator(context); dataTypesGenerator.Emit(); if (designOptions?.GenerateNodeManager == true) { new NodeManagerGenerator(context) { OverrideNamespace = designOptions.NodeManagerNamespace, OverrideClassName = designOptions.NodeManagerClassName, EmitFactory = designOptions.EmitNodeManagerFactory }.Emit(); new FluentBuilderGenerator(context) { OverrideManagerNamespace = designOptions.NodeManagerNamespace, OverrideManagerClassName = designOptions.NodeManagerClassName }.Emit(); } if (context.Options?.OmitObjectTypeProxies != true) { var objectTypeProxyGenerator = new ObjectTypeProxyGenerator(context); objectTypeProxyGenerator.Emit(); } // Event records are emitted only in the Stack path (alongside // the proxies) because they reference the EventRecord anchor // in Opc.Ua.Core. Models (Core.Types) skips them. var modelDependencyGenerator = new ModelDependencyGenerator(context); modelDependencyGenerator.Emit(); } } }