Files
UA-.NETStandard/Docs/CustomNodeSet2Workflow.md
Copilot 940a391664 Add documentation for custom NodeSet2 authoring workflow (#4170)
## Proposed changes

This change adds a focused guide for users building their own NodeSet2
models with the .NET Standard stack. It documents the intended workflow
discussed in the issue: author in ModelDesign, generate artifacts with
ModelCompiler, and create runtime instances in `CreateAddressSpace`
instead of hand-maintaining generated NodeSet2 content.

- **New workflow guide**
  - Added `Docs/CustomNodeSet2Workflow.md`
  - Documents the recommended split between:
    - `ModelDesign` as the source of truth
- ModelCompiler outputs (`NodeSet2`, CSV, generated classes/constants,
predefined nodes)
    - runtime instantiation in server code

- **Runtime guidance clarified**
- Explains when to use `CreateAddressSpace` vs.
`AddBehaviourToPredefinedNode`
- Calls out the common case of selecting one machine variant at runtime
from several modeled types
- Describes why generated instance children receive distinct NodeIds and
why manually reusing explicit NodeIds causes confusion

- **Modeling guidance clarified**
- Recommends making inherited optional children mandatory in the derived
type when the server always exposes them
- Positions generated `NodeSet2` as an output artifact, not the primary
hand-edited source

Example from the documented approach:

```xml
<opc:ObjectType SymbolicName="GearMachineType" BaseType="MachineTool:MachineToolType">
  <opc:Children>
    <opc:Property SymbolicName="DI:Model"
                  DataType="ua:LocalizedText"
                  ModellingRule="Mandatory" />
  </opc:Children>
</opc:ObjectType>
```

## Related Issues

## Types of changes

What types of changes does your code introduce?
_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR._

- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] Enhancement (non-breaking change which adds functionality)
- [ ] Test enhancement (non-breaking change to increase test coverage)
- [ ] Breaking change (fix or feature that would cause existing
functionality to not work as expected, requires version increase of
Nuget packages)
- [x] Documentation Update (if none of the other choices apply)

## Checklist

_Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to
ask. We're here to help! This is simply a reminder of what we are going
to look for before merging your code._

- [ ] I have read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf).
- [ ] I ran tests locally with my changes, all passed.
- [ ] I fixed all failing tests in the CI pipelines. 
- [ ] I fixed all introduced issues with CodeQL and LGTM.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added necessary documentation (if appropriate).
- [ ] Any dependent changes have been merged and published in downstream
modules.

## Further comments

The guide is anchored to existing quickstart patterns already present in
the repository, so it explains the current intended workflow without
introducing new tooling or changing runtime behavior.

<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes #4169

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-03 20:55:07 +00:00

6.9 KiB

General workflow for creating a custom NodeSet2

This document describes a practical workflow for creating and maintaining a custom OPC UA information model with the .NET Standard stack.

The short version is:

  1. Author the model in ModelDesign.
  2. Run ModelCompiler to generate the derived artifacts.
  3. Treat the generated NodeSet2, CSV, and code as build outputs.
  4. Instantiate your root objects in CreateAddressSpace.
  5. Keep editing the ModelDesign file instead of hand-editing the generated NodeSet2.

1. Start with ModelDesign

Use a ModelDesign file as the source of truth for your custom model.

That is the pattern used by the quickstart models in this repository. For example, the Boiler sample keeps the design in:

  • Applications/Quickstarts.Servers/Boiler/Generated/BoilerDesign.xml

From there, ModelCompiler generates the rest of the model artifacts.

ModelDesign is usually the easiest place to:

  • derive from a companion specification type,
  • add your own properties and objects,
  • change modelling rules from optional to mandatory for your subtype,
  • provide default values and descriptions.

For example, if your type derives from MachineTool:MachineToolType, you can redefine children in your subtype and mark the nodes you always want to exist as ModellingRule="Mandatory".

2. Generate the artifacts with ModelCompiler

Run ModelCompiler against the ModelDesign file and keep the generated outputs together.

Typical generated outputs are:

  • *.NodeSet2.xml
  • *.NodeIds.csv
  • *.Classes.cs
  • *.Constants.cs
  • *.DataTypes.cs
  • *.PredefinedNodes.xml
  • *.PredefinedNodes.uanodes

The Boiler sample shows this generated set in:

  • Applications/Quickstarts.Servers/Boiler/Generated

3. Treat NodeSet2 as generated output

The generated NodeSet2 can become much larger than the original ModelDesign file. That is expected.

The ModelDesign file is compact because it describes the model declaratively. The generated NodeSet2 is expanded and contains the fully materialized UA nodes, references, modelling rules, and metadata.

A good rule is:

  • edit ModelDesign.xml
  • regenerate NodeSet2.xml, CSV, and code
  • do not hand-maintain the generated NodeSet2 unless you have a very specific reason

4. Load the generated predefined nodes in your server

At runtime, the server usually loads the generated predefined nodes from the *.PredefinedNodes.uanodes resource.

The quickstart servers do this in LoadPredefinedNodes, for example:

  • Applications/Quickstarts.Servers/Boiler/BoilerNodeManager.cs
  • Applications/Quickstarts.Servers/MemoryBuffer/MemoryBufferNodeManager.cs

The generated binary node set is embedded as a resource in:

  • Applications/Quickstarts.Servers/Quickstarts.Servers.csproj

This is the part that brings the model metadata into the server in a form the node manager can use.

5. Create your runtime root objects in CreateAddressSpace

Use CreateAddressSpace to decide which concrete objects should exist in the running server.

This is the right place to:

  • create one machine instance for the current device,
  • choose between multiple machine variants,
  • attach the instance to the Objects folder or another predefined root,
  • initialize values and runtime behavior.

This pattern is used throughout the quickstart servers. For example:

  • Applications/Quickstarts.Servers/Boiler/BoilerNodeManager.cs
  • Applications/Quickstarts.Servers/MemoryBuffer/MemoryBufferNodeManager.cs

If you have three machine variants, define the reusable types in ModelDesign, then instantiate only the one that applies to the current machine in CreateAddressSpace.

If your NodeSet already contains concrete predefined objects and you need to attach custom runtime behavior to them, use AddBehaviourToPredefinedNode. If runtime configuration decides which root objects exist, CreateAddressSpace is usually the clearer workflow.

6. Prefer modelling optional children in your subtype when you always need them

If a companion specification defines a child as optional but your machine always exposes it, model it as mandatory in your derived type.

That avoids manually creating each optional child in code and gives you the generated metadata, references, and structure automatically.

This is often simpler than creating optional properties by hand with new PropertyState(...) and then trying to recreate the missing metadata yourself.

7. Let each instance get its own instance NodeIds

When child nodes are introduced through the type definition and then instantiated as part of an object instance, each instantiated child receives its own instance NodeId.

That is normally what you want.

The important distinction is:

  • type definition nodes describe the structure,
  • instance nodes are the concrete runtime nodes created from that structure.

If multiple tools appear to share one manually assigned NodeId, that usually means the node was modeled or created as a single explicit node instead of as part of the instantiated type structure.

Modeling the tool structure in ModelDesign and then instantiating the tool object avoids that problem.

Practical guidance

For most custom server projects, the clean workflow is:

  1. Define your types in ModelDesign.
  2. Derive from companion specification types there.
  3. Mark children as mandatory in your subtype when your server always needs them.
  4. Run ModelCompiler.
  5. Commit the generated artifacts required by your project.
  6. Load the predefined nodes in the node manager.
  7. Create the concrete root object for the current machine in CreateAddressSpace.
  8. Update values and behavior in your runtime code.

When to edit which file

Edit the ModelDesign file when you want to change

  • the type hierarchy,
  • references,
  • modelling rules,
  • default values,
  • browse names,
  • custom types and properties.

Edit C# server code when you want to change

  • which machine instance is created at runtime,
  • how values are updated,
  • method implementations,
  • eventing and runtime behavior,
  • environment-specific configuration.

Avoid editing the generated NodeSet2 directly when

  • the same change can be expressed in ModelDesign,
  • you plan to regenerate the model later,
  • you need consistency between XML, CSV, and generated code.
  • Applications/Quickstarts.Servers/Boiler/Generated/BoilerDesign.xml
  • Applications/Quickstarts.Servers/Boiler/Generated/Boiler.NodeSet2.xml
  • Applications/Quickstarts.Servers/Boiler/BoilerNodeManager.cs
  • Applications/Quickstarts.Servers/MemoryBuffer/MemoryBufferNodeManager.cs

Summary

For this stack, the most maintainable workflow is usually:

  • ModelDesign as the source of truth
  • ModelCompiler for generated artifacts
  • NodeSet2 as generated output
  • CreateAddressSpace for choosing and creating runtime instances

That keeps the model definition, generated artifacts, and runtime logic clearly separated.