Files
UA-.NETStandard/docs/GettingStarted.md
Marc Schier c4e0721a29 Add a documentation learning path and fix issues found while verifying it (#4555)
## Summary

### Documentation
- Stage the documentation index as a learning path: a first application,
independent client, server, and PubSub paths, then data and services,
companion models, production, advanced integration, and draft or
experimental capabilities.
- Add Getting started (repository samples or NuGet packages), OPC UA
concepts, Your first information model, and a production readiness
checklist.
- Add Native AOT application publishing, a first PubSub publisher and
subscriber exchange, and the UDP transport options.
- Add or expand contents navigation throughout `docs/`, and keep
contributor documentation separate.
- Correct outdated SDK examples and capability guidance, consolidate
repeated explanations into canonical guides, and repair local links and
heading structure.
- Rewrite What's New in 2.0 as a concise feature overview with short
linked details; keep migration instructions in the Migration Guide.
- Expand the feature catalogue with specification IDs, GDS, LDS, the OPC
10000-21 onboarding registrar, ISA-95, xRegistry, OpenUSD, and
sample-local models; distinguish draft status, explain crypto
pluggability, and bring performance guidance forward.
- Address review feedback on wording, active monitored items during
shadow reload, and the `Samples.md` filename and references.
- Regenerate the `opcua-v20-migration` plugin's bundled copy of the
migration guides so it matches the edited `docs/migrate/2.0.x` guides.
- Merge the latest `master`, preserving upstream resource-isolation
guidance within the task-based documentation index.
- Retain deferred Lingity findings where appropriate; this PR does not
claim that every document meets the Lingity score thresholds.

### Fixes found while verifying the guides
Each fix has regression tests that fail without it.
- **Client:** V2 subscriptions compared their lifetime with the
operation timeout instead of the session timeout, and logged a spurious
warning. `ISubscriptionEngineContext`, which is new in 2.0, gains a
`SessionTimeout` property; implementations must add it.
- **PubSub:** a subscriber now registers the `DataSetMetaData`
configured on its readers (OPC 10000-14 6.2.9.4), and the UADP decoder
decodes DataSetMessages that carry no ConfigurationVersion. Before, a
secured subscriber of the reference sample received no DataSets.
- **Source generator:** a ModelDesign method that declares its arguments
inline, without a method type, produced code that did not compile; it
now uses `MethodState`. The generated code of existing models is
unchanged.
- **MinimalClient sample:** waits for its first data-change notification
before it disconnects.

### Known issues, not addressed here
- UDP multicast: the publisher does not send on the interface that the
subscriber joins, and `MulticastLoopback` defaults to false. The PubSub
guide uses unicast for the exchange on one computer.
- The UDP metadata announcement is sent unsecured on secured writer
groups; a secured subscriber drops it with a warning.
- A server does not start in globalization-invariant mode, because
`ResourceManager` creates `CultureInfo("en-US")`. The Native AOT guide
cautions against the mode.

## Validation
- `git diff --check` and
`./.azurepipelines/validate-migration-plugin.ps1` (15 bundled migration
docs validated, plugin manifests synchronized).
- Local Markdown links, anchors, file-link casing, and contents lists
checked across all 87 documents under `docs/`. The index links every
top-level guide and every target of the previous index.
- Both Getting started routes, Your first information model, the PubSub
exchange, and the Native AOT publish of the Getting started client and
server were run as documented. The NuGet route and the tutorial use the
public `2.0.0-preview.5` packages.
- The changed libraries build for all target frameworks in Release
without warnings.
- Tests on .NET 10: Opc.Ua.SourceGeneration.Tests 199 passed;
Opc.Ua.SourceGeneration.Core.Tests 3,977 passed, 8 skipped;
Opc.Ua.Server.Tests `Fluent` category 621 passed; Opc.Ua.Client.Tests
subscription-engine tests 70 passed; Opc.Ua.PubSub.Tests 1,374 passed, 2
skipped; PubSub fuzz replay 145 passed. The new and affected tests also
pass on .NET Framework 4.8.
- Earlier merge validation: 174 resource-isolation tests passed on .NET
10.
- The full solution suite was not run locally.

Review and validation artifacts are maintained outside the repository.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-09-28 19:19:39 +00:00

20 KiB

Getting started

This guide gets an OPC UA client and server talking on one computer. Choose one of two entry routes:

Both routes run on your computer without other servers, brokers, credentials, or cloud accounts. They show slightly different parts of the first workflow:

Outcome Route A Route B
Encrypted connection (SignAndEncrypt, Basic256Sha256) Yes Yes
Certificate trust The client trusts the server explicitly. The sample server accepts clients automatically. Each application trusts the other explicitly.
Browse the Objects folder Yes, including the sample's Boiler model Yes, standard server nodes only
Read a value and check its status Yes Yes
Receive data-change notifications One notification Three notifications

After either route, what the code does explains each step. The concepts guide explains the underlying OPC UA terms.

Contents

Before you begin

You need two terminal windows. The commands are shown for PowerShell and for bash where they differ.

Requirement Route A Route B
.NET SDK The exact version in global.json. Roll-forward is disabled, so other SDK versions are refused. .NET 10 SDK.
Other requirements Git, and internet access to restore NuGet packages on the first build Internet access to nuget.org
TCP port 62541 62560

Route B was verified with package version 2.0.0-preview.5. The packages are prereleases, so dotnet add package needs the --prerelease option.

Route A: Run the repository samples

Route A uses two samples from samples/MinimalApi:

  • MinimalBoilerServer hosts a small Boiler model. Its node manager is generated from the model at build time.
  • MinimalClient discovers the server's encrypted endpoint, creates a subscription, browses, reads, and waits for the subscription's first data-change notification.

A1. Start the server

git clone https://github.com/OPCFoundation/UA-.NETStandard.git
cd UA-.NETStandard
dotnet run --project samples/MinimalApi/MinimalBoilerServer

The first build compiles the stack and can take a few minutes. The server is ready when it logs:

OPC UA server listening at opc.tcp://localhost:62541/MinimalBoilerServer.

Leave the server running.

Local sample only: MinimalBoilerServer sets AutoAcceptUntrustedCertificates = true, so it accepts every client certificate and logs Auto accepted certificate. Never use this setting in production. Route B shows how a server trusts a client explicitly.

A2. Run the client and trust the server

In the second terminal, at the repository root, run:

dotnet run --project samples/MinimalApi/MinimalClient

The client does not trust the new server certificate yet. It logs BadCertificateUntrusted, saves the server certificate in its rejected store, and keeps retrying. While it retries, move the certificate to the client's trusted store. The client keeps its stores in the system temporary directory.

PowerShell:

$pki = Join-Path ([IO.Path]::GetTempPath()) 'OPC Foundation/MinimalClient/pki'
New-Item -ItemType Directory -Force "$pki/trusted/certs" | Out-Null
Move-Item "$pki/rejected/certs/*.der" "$pki/trusted/certs"

bash:

pki="${TMPDIR:-/tmp}/OPC Foundation/MinimalClient/pki"
mkdir -p "$pki/trusted/certs"
mv "$pki"/rejected/certs/*.der "$pki/trusted/certs/"

These commands move every certificate the client has rejected. In this exercise, that is only the MinimalBoilerServer certificate. Outside a local test, confirm a certificate's thumbprint with the server's administrator before you trust it; the thumbprint is part of the file name.

The client retries with an increasing delay of up to 30 seconds, so it connects within about half a minute after you move the certificate. You can also stop it with Ctrl+C and run it again. If it cannot connect for five minutes, it gives up.

A3. Check the result

After the rejection messages, the client prints output like this:

Using endpoint: opc.tcp://localhost:62541/MinimalBoilerServer
Security mode: SignAndEncrypt
Security policy: http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256
Connected!

A&C client ready: AlarmClient
Subscription state: Opened

Browsing Objects folder...
Subscription state: Created
Found 4 references
  - Server (Object)
  - Aliases (Object)
  - Locations (Object)
  - Boilers (Object)

Reading ServerStatus.CurrentTime...
Server time: 28/09/2026 11:43:42

Waiting for a data change notification...
Subscription state: Modified
Subscription value: 28/09/2026 11:43:43

Disconnecting...
Subscription state: Deleted
Subscription state: Opened
Done

Boilers is the folder of the sample's generated model; the other three nodes are standard server nodes. Subscription value is the data-change notification for the monitored server time. The times and their format depend on your computer.

A4. Stop and clean up

Press Ctrl+C in the server terminal. The samples keep their certificates, including private keys, in these folders of the system temporary directory:

  • OPC Foundation/MinimalBoilerServer/pki
  • OPC Foundation/MinimalClient/pki

Delete them to repeat the exercise with new certificates.

Route B: Build applications from NuGet packages

In Route B, you create two console applications:

  • A server that exposes only the standard OPC UA nodes.
  • A client that browses and reads the server, then monitors a value.

Each application keeps its certificate stores in a pki folder in its project folder. Run each application from its own project folder so that it finds that pki folder.

B1. Create the server

dotnet new console -n GettingStartedServer
cd GettingStartedServer
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Server --prerelease
dotnet add package Microsoft.Extensions.Hosting

Replace the contents of Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services
    .AddOpcUa()
    .AddServer(options =>
    {
        options.ApplicationName = "GettingStartedServer";
        options.ApplicationUri = "urn:localhost:GettingStartedServer";
        options.ProductUri = "urn:example.com:GettingStartedServer";
        options.EndpointUrls.Add("opc.tcp://localhost:62560/GettingStartedServer");
        options.PkiRoot = Path.Combine(builder.Environment.ContentRootPath, "pki");
    });

await builder.Build().RunAsync();

Start the server:

dotnet run

On first start, the server creates its application certificates in pki. It is ready when it logs:

OPC UA server listening at opc.tcp://localhost:62560/GettingStartedServer.

Leave the server running.

B2. Create the client

In the second terminal, go to the folder that contains GettingStartedServer and create the client next to it:

dotnet new console -n GettingStartedClient
cd GettingStartedClient
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client --prerelease
dotnet add package Microsoft.Extensions.Hosting

Replace the contents of Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions;
using Opc.Ua.Client.Subscriptions.Streaming;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.SetMinimumLevel(LogLevel.Warning);

builder.Services
    .AddOpcUa()
    .AddClient(options =>
    {
        options.ApplicationName = "GettingStartedClient";
        options.ApplicationUri = "urn:localhost:GettingStartedClient";
        options.ProductUri = "urn:example.com:GettingStartedClient";
        options.PkiRoot = Path.Combine(builder.Environment.ContentRootPath, "pki");
        // Report connection problems quickly while you set up trust. Long-running
        // clients usually keep the default policy, which retries for minutes.
        options.Session = new ManagedSessionOptions
        {
            ReconnectPolicy = new ReconnectPolicyOptions { MaxRetries = 2 },
        };
    })
    .AddDiscoveryAndConnect(options =>
    {
        options.DiscoveryUrl = "opc.tcp://localhost:62560/GettingStartedServer";
        options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
        options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
    });

using IHost host = builder.Build();
await host.StartAsync();

using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(1));
CancellationToken ct = timeout.Token;

try
{
    var connect = host.Services
        .GetRequiredService<Func<CancellationToken, Task<ManagedSession>>>();
    ManagedSession session = await connect(ct);
    await using (session)
    {
        Console.WriteLine($"Connected to {session.ConfiguredEndpoint?.EndpointUrl}");

        // Browse the standard Objects folder.
        var browser = new Browser(session)
        {
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
            IncludeSubtypes = true,
            NodeClassMask = (uint)(NodeClass.Object | NodeClass.Variable),
        };
        ArrayOf<ReferenceDescription> children =
            await browser.BrowseAsync(ObjectIds.ObjectsFolder, ct);
        Console.WriteLine("Objects folder:");
        foreach (ReferenceDescription child in children)
        {
            Console.WriteLine($"  {child.DisplayName} ({child.NodeClass})");
        }

        // Read the server clock. Check the status before using the value.
        DataValue value = await session.ReadValueAsync(
            VariableIds.Server_ServerStatus_CurrentTime,
            ct);
        PrintTime("Read", value);

        // Monitor the same variable and stop after three notifications.
        Console.WriteLine("Waiting for three data changes...");
        await foreach (DataValueChange change in session.DefaultStreaming
            .SubscribeDataChangesAsync(VariableIds.Server_ServerStatus_CurrentTime, ct: ct)
            .TakeAsync(3)
            .WithCancellation(ct))
        {
            PrintTime("Update", change.Value);
        }
    }
}
catch (ServiceResultException ex)
{
    Console.Error.WriteLine($"OPC UA error {ex.StatusCode}: {ex.Message}");
    Environment.ExitCode = 1;
}
catch (OperationCanceledException)
{
    Console.Error.WriteLine("The client did not finish within one minute.");
    Environment.ExitCode = 1;
}
finally
{
    await host.StopAsync();
}

static void PrintTime(string label, DataValue value)
{
    if (StatusCode.IsBad(value.StatusCode))
    {
        Console.WriteLine($"{label}: failed with {value.StatusCode}");
    }
    else if (value.WrappedValue.TryGetValue(out DateTimeUtc time))
    {
        Console.WriteLine($"{label}: {time} (status {value.StatusCode})");
    }
    else
    {
        Console.WriteLine($"{label}: unexpected value {value.WrappedValue}");
    }
}

B3. Trust each application

A new application trusts no other application. Each time a connection fails because of an untrusted certificate, the application that refused it saves the certificate in its pki/rejected/certs folder. You approve a certificate by moving it to pki/trusted/certs. Both applications read their trusted store again when it changes, so you do not need to restart the server.

  1. From the GettingStartedClient folder, run dotnet run. After some log messages, the client stops with:

    OPC UA error BadSecurityChecksFailed [0x80130000]: ...
    

    The server has refused the client certificate. In the GettingStartedServer folder, trust it:

    PowerShell:

    New-Item -ItemType Directory -Force pki/trusted/certs | Out-Null
    Move-Item pki/rejected/certs/*.der pki/trusted/certs
    

    bash:

    mkdir -p pki/trusted/certs
    mv pki/rejected/certs/*.der pki/trusted/certs/
    
  2. Run the client again. It now stops with:

    OPC UA error BadCertificateUntrusted [0x801A0000]: ...
    

    This time, the client has refused the server certificate. Run the same trust commands in the GettingStartedClient folder.

  3. Run the client a third time. It connects and completes.

B4. Check the result

The client prints output like this and exits with exit code 0:

Connected to opc.tcp://localhost:62560/GettingStartedServer
Objects folder:
  Server (Object)
  Aliases (Object)
  Locations (Object)
Read: 09/28/2026 09:28:36 (status Good [0x00000000])
Waiting for three data changes...
Update: 09/28/2026 09:28:36 (status Good [0x00000000])
Update: 09/28/2026 09:28:37 (status Good [0x00000000])
Update: 09/28/2026 09:28:38 (status Good [0x00000000])

The times depend on when you run the client. Before the output, the client logs several Application Certificate Validation suppressed BadCertificateUntrusted warnings. They come from the startup check of the client's own self-signed certificates, not from the server certificate, and need no action.

B5. Stop and clean up

Press Ctrl+C in the server terminal. Each pki folder contains private keys. Do not commit them to source control. Delete both pki folders to repeat the exercise with new certificates.

Connect to another server

To use the client with another server, change DiscoveryUrl to that server's URL. Set SecurityMode and SecurityPolicyUri to values that one of its endpoints offers. Trust the server certificate as in B3, and ask the server's administrator to trust your client certificate from GettingStartedClient/pki/certs. The client connects as an anonymous user; if the server requires user authentication, see Identity providers. The client only browses, reads, and monitors; it does not change the server.

What the code does

Step Code Learn more
Identify the application ApplicationName, ApplicationUri, and ProductUri name the application. PkiRoot selects its certificate stores. The first start creates an RSA certificate and, where the platform supports them, ECC certificates. Concepts: applications, Certificates
Host the server AddServer runs the server as a hosted service. By default, it offers SignAndEncrypt endpoints with RSA security policies, does not offer the insecure None policy, and allows anonymous users. Dependency injection
Select an endpoint AddDiscoveryAndConnect calls GetEndpoints on the URL and selects the endpoint with the configured security mode and policy. Concepts: endpoints
Connect The registered Func<CancellationToken, Task<ManagedSession>> connects a ManagedSession, which reconnects automatically after connection loss. Sessions
Browse Browser follows hierarchical references from the Objects folder. Concepts: address space
Read ReadValueAsync returns a DataValue whose status is Good or Uncertain, and throws a ServiceResultException when the status is Bad. Concepts: values
Monitor session.DefaultStreaming is the session's shared streaming subscription. The first SubscribeDataChangesAsync call creates the OPC UA subscription and adds a monitored item. TakeAsync(3) ends the stream after three values and removes the monitored item. Streaming subscriptions
Clean up await using closes the session and its subscription. The timeout bounds the whole run. Concepts: SDK organization

The client limits reconnect attempts to two so that trust problems appear quickly. A long-running client usually keeps the default policy, which retries for several minutes. Some settings suit local use only: automatic certificate acceptance in the Route A server, and certificate stores in a temporary or project folder. Production applications keep their certificate stores in a persistent, access-controlled location; see the production readiness checklist.

Troubleshooting

Symptom Cause What to do
BadSecurityChecksFailed with Could not verify security on OpenSecureChannel request The server does not trust the client certificate. Move the client certificate from the server's rejected store to its trusted store.
BadCertificateUntrusted with Could not verify security on OpenSecureChannel response The client does not trust the server certificate. Move the server certificate from the client's rejected store to its trusted store.
BadNotConnected with Could not connect to the remote endpoint; with the 2.0.0-preview.5 packages, a SocketException such as No connection could be made because the target machine actively refused it No server is listening at the URL. Start the server, then check the host, port, and path in the URL.
No discovered endpoint matched the configured security policy and mode. The server does not offer the requested security mode and policy. Choose a mode and policy that the server offers. See Profiles.
The server does not start, and the port is in use Another application uses the port. Stop that application, or change the port in both the server endpoint URL and the client discovery URL.
A compatible .NET SDK was not found (Route A) The SDK version in global.json is not installed. Install that SDK version.

See Certificates for certificate stores and chains, and Diagnostics for logging and packet capture.

Next steps

  • Learn the terms used throughout the documentation in OPC UA concepts.
  • Add your own model to the Route B server, and read, write, and call it from the client, in Your first information model.
  • Continue on the path for your application in the documentation index: a client, a server with your own model, or a PubSub application.
  • Before you deploy, work through the production readiness checklist: certificate management, user authentication, resource limits, and diagnostics.