diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/README.md b/.agents/skills/opcua-v20-migration/references/stack-migration/README.md index c4a8888578..fc08dd264a 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/README.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/README.md @@ -29,6 +29,12 @@ find the sub-doc that matches the symptom you are seeing. > skill, which knows when to load each sub-doc and runs the codefixer > for you. +## Contents + +- [Migration sub-doc index](#migration-sub-doc-index) +- [All sub-documents](#all-sub-documents) +- [See also](#see-also) + ## Migration sub-doc index Find the row that matches the error / API surface you are dealing with diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/alarms-model-change.md b/.agents/skills/opcua-v20-migration/references/stack-migration/alarms-model-change.md index eaff2a8aed..89cca1452f 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/alarms-model-change.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/alarms-model-change.md @@ -2,6 +2,14 @@ > **When to read this:** Read this for `AlarmConditionState` state-transition behaviour, the auto-emitted `GeneralModelChangeEvent` from `CustomNodeManager.CreateNode/DeleteNode`, and the address-space `ModelChangeAggregator` migration. +## Contents + +- [Alarms and Conditions](#alarms-and-conditions) + - [`AlarmConditionState` state-transition behavior](#alarmconditionstate-state-transition-behavior) + - [Auto-emit `GeneralModelChangeEvent` from `CustomNodeManager`](#auto-emit-generalmodelchangeevent-from-customnodemanager) +- [Address-space model change tracking](#address-space-model-change-tracking) + - [New `INodeCache.InvalidateNode` member](#new-inodecacheinvalidatenode-member) + ## Alarms and Conditions Two changes require attention. diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/certificates.md b/.agents/skills/opcua-v20-migration/references/stack-migration/certificates.md index f4b7220a6f..153697ebbd 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/certificates.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/certificates.md @@ -2,6 +2,21 @@ > **When to read this:** Read this for the new ref-counted `Certificate` wrapper, the segregated-interface `CertificateManager` design, the `ICertificateProvider` cache, and the obsoleted `X509Certificate2` direct-exposure APIs. +## Contents + +- [Centralised certificate cache via `ICertificateProvider`](#centralised-certificate-cache-via-icertificateprovider) +- [Certificate Management](#certificate-management) + - [ECC security policies require .NET 8 or later](#ecc-security-policies-require-net-8-or-later) + - [Certificates with an empty distinguished name are always rejected](#certificates-with-an-empty-distinguished-name-are-always-rejected) + - [Certificate and CertificateCollection wrapper types](#certificate-and-certificatecollection-wrapper-types) + - [CertificateManager and segregated interfaces](#certificatemanager-and-segregated-interfaces) + - [CertificateIdentifier is metadata-only](#certificateidentifier-is-metadata-only) + - [CertificateStoreIdentifier is a store description — `OpenStore` returns a caller-owned store](#certificatestoreidentifier-is-a-store-description--openstore-returns-a-caller-owned-store) + - [PushManagement transactions: TrustList/Certificate updates now require `ApplyChanges`](#pushmanagement-transactions-trustlistcertificate-updates-now-require-applychanges) + - [Optional ServerConfiguration surface now available (additive, opt-in)](#optional-serverconfiguration-surface-now-available-additive-opt-in) + - [`MaxTrustListSize` is advertised honestly and bounded by a safety ceiling](#maxtrustlistsize-is-advertised-honestly-and-bounded-by-a-safety-ceiling) + - [Obsoleted certificate APIs](#obsoleted-certificate-apis) + ## Centralised certificate cache via `ICertificateProvider` A new public `ICertificateProvider` interface exposes the existing @@ -235,7 +250,7 @@ through the new `ServerConfigurationOptions` argument on Previously, a `ServerConfiguration.MaxTrustListSize` of `0` (unlimited per OPC UA Part 12 §8.4.5) was advertised to Clients as `0` while the server silently -enforced a hidden 1 MiB cap on TrustList `Read`/`Write`. The advertised +enforced a hidden 1 MiB cap on TrustList `Read`/`Write`. The advertised value and the enforced value could therefore disagree. The server now: @@ -243,17 +258,17 @@ The server now: - **Advertises the honest effective limit.** `ServerConfiguration.MaxTrustListSize` now reports the value the TrustList handlers actually enforce — never `0` while a finite cap is in force. A server configured with `MaxTrustListSize = 0` - now advertises the safety ceiling (default 1 MiB) instead of `0`. + now advertises the safety ceiling (default 1 MiB) instead of `0`. - **Adds a configurable resource-protection safety ceiling.** `ServerConfigurationOptions.MaxTrustListSizeSafetyCeiling` (default - 1 MiB) bounds the actually-enforced size. The effective limit is: + 1 MiB) bounds the actually-enforced size. The effective limit is: `MaxTrustListSize == 0` → the ceiling; `MaxTrustListSize` above the ceiling → the ceiling; otherwise → the configured `MaxTrustListSize`. **Migration action.** No action is required for the common cases -(`MaxTrustListSize` of `0` or a finite value ≤ 1 MiB); enforcement is +(`MaxTrustListSize` of `0` or a finite value ≤ 1 MiB); enforcement is unchanged and only the advertised value becomes honest. **If you configured a -finite `MaxTrustListSize` larger than 1 MiB**, raise +finite `MaxTrustListSize` larger than 1 MiB**, raise `ServerConfigurationOptions.MaxTrustListSizeSafetyCeiling` to at least that value, otherwise the effective limit is clamped to the ceiling: @@ -267,12 +282,12 @@ builder.ConfigureServerConfiguration(o => The legacy `TrustList` constructor overloads (without an explicit safety ceiling) are unchanged and remain fully backward compatible: a finite size is -honored exactly (never clamped) and `0` falls back to the 1 MiB default. +honored exactly (never clamped) and `0` falls back to the 1 MiB default. ### Obsoleted certificate APIs -The following APIs are marked `[Obsolete]` and will be removed in the next minor version. They remain -functional forwarders to the new design for binary-compatibility, but emit `CS0618` warnings when used. +The following static factory methods remain `[Obsolete]` forwarders and emit +`CS0618` when used. Migrate them to the instance-based factory and issuer APIs. | Obsolete API | Replacement | |-------------|-------------| @@ -283,16 +298,25 @@ functional forwarders to the new design for binary-compatibility, but emit `CS06 | `CertificateFactory.RevokeCertificate(...)` | `DefaultCertificateIssuer.Instance.RevokeCertificates(...)` | | `CertificateFactory.CreateCertificateWithPEMPrivateKey(...)` | `DefaultCertificateFactory.Instance.CreateWithPEMPrivateKey(...)` | | `CertificateFactory.CreateCertificateWithPrivateKey(...)` | `DefaultCertificateFactory.Instance.CreateWithPrivateKey(...)` | -| `CertificateStoreIdentifier.RegisterCertificateStoreType(...)` | Register `ICertificateStoreProvider` via dependency injection or pass to the `CertificateManager` constructor | +| `CertificateStoreType.RegisterCertificateStoreType(...)` | Register `ICertificateStoreProvider` through dependency injection or pass it to `CertificateManager`. This obsolete registration API is not a factory forwarder. | + +#### Removed certificate APIs + +The following types and properties are removed, not obsolete forwarders. +Update callers before compiling against 2.0. + +| Removed API | Replacement | +|-------------|-------------| | `CertificateValidator` (class) | `ICertificateManager` (composed of `ICertificateValidatorEx` for validation, `ICertificateRegistry` for app certs, `ICertificateTrustListManager` for trust lists, `ICertificateLifecycle` for change events). Construct via `CertificateManagerFactory.Create(securityConfiguration, telemetry, ...)` | | `ICertificateValidator` (interface) | `ICertificateValidatorEx` from `ICertificateManager`. The new interface returns a structured `CertificateValidationResult` (`IsValid`, `StatusCode`, `Errors`, `IsBeingTrustedTransiently`) instead of throwing. Per-error accept logic moves from the `CertificateValidation` event to the new `CertificateValidationOptions.AcceptError` callback. | | `CertificateTypesProvider` (class) | `ICertificateRegistry` (composed in `ICertificateManager`). Use `using CertificateEntry? e = manager.AcquireApplicationCertificateBySecurityPolicy(securityPolicyUri);` (caller-owned — dispose the entry). The entry already carries the chain: use `e.IssuerChain` / `e.GetEncodedChainBlob()`. | -| `ApplicationConfiguration.CertificateValidator` (property) | `ApplicationConfiguration.CertificateManager` (parallel property — set in `ApplicationInstance.CheckApplicationInstanceCertificatesAsync`) | +| `CertificateValidatorAdapter` (class) | No legacy bridge; consume `ICertificateValidatorEx` or `ICertificateManager` directly. | +| `ApplicationConfiguration.CertificateValidator` (property) | `ApplicationConfiguration.CertificateManager` (set in `ApplicationInstance.CheckApplicationInstanceCertificatesAsync`) | | `ServerBase.CertificateValidator` (property) | `ServerBase.CertificateManager` | | `ServerBase.InstanceCertificateTypesProvider` (property) | `ServerBase.CertificateManager` (use `ICertificateRegistry` surface) | > **Lifecycle ordering.** `configuration.CertificateManager` is populated *inside* `await applicationInstance.CheckApplicationInstanceCertificatesAsync(...)`. Code that reads it before that call gets `null`. The required ordering is: -> + > 1. Construct `new ApplicationInstance(telemetry)`. > 2. Load `ApplicationConfiguration` (e.g. via `LoadApplicationConfigurationAsync`). > 3. `await applicationInstance.CheckApplicationInstanceCertificatesAsync(silent: false, ..., ct);`. @@ -300,8 +324,10 @@ functional forwarders to the new design for binary-compatibility, but emit `CS06 #### Migrating the `CertificateValidator.CertificateValidation` event -The legacy event with mutable `e.Accept = true` mutability has been replaced by -the structured `CertificateValidationOptions.AcceptError` callback: +The legacy event with mutable `e.Accept = true` has been replaced by +the structured `CertificateValidationOptions.AcceptError` callback. The following +untrusted-certificate exception is for controlled development only; configure +trust stores instead in production. ```csharp // Before: @@ -321,7 +347,7 @@ var options = new CertificateValidationOptions error.StatusCode == StatusCodes.BadCertificateUntrusted }; CertificateValidationResult result = - await applicationInstance.CertificateManager.ValidateAsync(cert, options: options); + await configuration.CertificateManager.ValidateAsync(cert, options: options); if (!result.IsValid) { throw new ServiceResultException(result.StatusCode); @@ -333,17 +359,16 @@ if (!result.IsValid) `CertificateValidator.ValidateApplicationUri(...)` and `CertificateValidator.ValidateDomains(...)` are now exposed as extension methods on `ICertificateValidatorEx` in the -`Opc.Ua.CertificateValidationExtensions` static class. Existing call sites -that previously used the legacy class continue to work transparently. +`Opc.Ua.CertificateValidationExtensions` static class. Change the receiver to +`configuration.CertificateManager`; the removed legacy class is not a +compatibility shim. > The `CertificateFactory.DefaultKeySize` / `DefaultLifeTime` / `DefaultHashSize` constants are > intentionally **not** marked obsolete; they remain the canonical default values used across > configuration sites. -To suppress `CS0618` warnings while migrating, add at the top of affected files: -```csharp -#pragma warning disable CS0618 // Obsolete API usage during migration -``` +Replace the obsolete calls rather than suppressing `CS0618` across a file. +Warnings for forwarders do not imply that the removed APIs above are available. --- diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/configuration.md b/.agents/skills/opcua-v20-migration/references/stack-migration/configuration.md index 25b077b857..cd6ed15513 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/configuration.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/configuration.md @@ -2,6 +2,18 @@ > **When to read this:** Read this for `ApplicationConfiguration` changes, the removed Data-Contract serializer, Newtonsoft removal from `Opc.Ua.Core`, the new `ParseExtension` / `UpdateExtension` signature, and session / browser state persistence. +## Contents + +- [Configuration](#configuration) + - [Data Contract Serializer support removed](#data-contract-serializer-support-removed) + - [TraceConfiguration apply APIs removed](#traceconfiguration-apply-apis-removed) + - [MinMetadataSamplingInterval removed, MinSupportedSamplingInterval added](#minmetadatasamplinginterval-removed-minsupportedsamplinginterval-added) + - [Newtonsoft.Json removed from Opc.Ua.Core](#newtonsoftjson-removed-from-opcuacore) + - [ParseExtension/UpdateExtension signature changed](#parseextensionupdateextension-signature-changed) + - [ExtensionObject array helpers changed](#extensionobject-array-helpers-changed) + - [IJsonEncodeable interface removed](#ijsonencodeable-interface-removed) +- [Session and Browser State Persistence](#session-and-browser-state-persistence) + ## Configuration ### Data Contract Serializer support removed diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/encoders.md b/.agents/skills/opcua-v20-migration/references/stack-migration/encoders.md index 48b758bef8..5bfae487c7 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/encoders.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/encoders.md @@ -1,6 +1,18 @@ # Encoders and Complex Types -> **When to read this:** Read this for `IEncodeableFactoryBuilder` / `IType` / `EncodeableFactory.GlobalFactory` migration, JSON / XML / binary encoder/decoder changes (removed Default JSON encoding infrastructure, `IJsonEncodeable` removal), and complex-types moves to `Opc.Ua.Client`. +> **When to read this:** Read this for `IEncodeableFactoryBuilder` / `IType` / `EncodeableFactory.GlobalFactory` migration, JSON / XML / binary encoder/decoder changes (removed Default JSON encoding infrastructure, `IJsonEncodeable` removal), and the shared complex-type system in `Opc.Ua.Core.Schema` and its client-side factory helpers. + +## Contents + +- [Encodeable Factory and Complex Type System](#encodeable-factory-and-complex-type-system) + - [IType hierarchy](#itype-hierarchy) + - [IEncodeableTypeLookup changes](#iencodeabletypelookup-changes) + - [IEncodeableFactoryBuilder changes](#iencodeablefactorybuilder-changes) + - [EncodeableFactory.GlobalFactory removed](#encodeablefactoryglobalfactory-removed) +- [Encoders and Decoders](#encoders-and-decoders) +- [Complex Types](#complex-types) + - [ComplexTypes moved to Opc.Ua.Core.Schema](#complextypes-moved-to-opcuacoreschema) + - [OptionSet DataType support](#optionset-datatype-support) ## Encodeable Factory and Complex Type System @@ -26,22 +38,8 @@ New type abstraction layer: `IType` (base) with `IBuiltInType`, `IEnumeratedType The `[Obsolete]` static `EncodeableFactory.GlobalFactory` was removed. `EncodeableFactory.Create()` renamed to `Fork()`. Use `ServiceMessageContext.Factory` instead. -### ComplexTypes moved to Opc.Ua.Core.Schema - -The shared `ComplexTypeSystem` orchestrator, the complex type interfaces and the default (non-reflection-emit) type builder moved to the `Opc.Ua.Core.Schema` assembly under the root `Opc.Ua` namespace (which consumers already import) so they can be used by both client and server and existing code keeps compiling without adding a new `using`. Remove the old `Opc.Ua.Client.ComplexTypes` import if it is now unused (the client-only `NodeCacheResolver` and the `ComplexTypeSystem.Create(session, ...)` helpers stay in `Opc.Ua.Client.ComplexTypes`). -The `ComplexTypeSystem(ISession, ...)` constructors were removed; construct a session-bound instance with `ComplexTypeSystem.Create(session, telemetry)` (the default, NativeAOT friendly builder) or `ComplexTypeSystem.Create(session, new ComplexTypeBuilderFactory(), telemetry)` for the Reflection.Emit builder. `ComplexTypeSystem` is now `IDisposable`: an instance from `Create` (or from `IComplexTypeSystemFactory.Create`) owns the `NodeCacheResolver` and its node cache, so dispose it once no more types are loaded through it; loaded types stay registered. -Servers build the same stand-ins for runtime-loaded DataTypes **by default** (`StandardServer.LoadComplexTypes`; opt out by setting it to `false`); configure the pass with `AddComplexTypeSystem()` or invoke `IServerInternal.LoadComplexTypesAsync(...)` directly. See `docs/ComplexTypes.md`. - -### OptionSet DataType support - -Concrete Structure-backed sub-types of the abstract `OptionSet` DataType (`i=12755`) are now automatically registered by the default `ComplexTypeSystem` builder with a new runtime class `Opc.Ua.Encoders.OptionSet` (in `src/Opc.Ua.Types`). Bit-field metadata is resolved from `DataTypeDefinition` (`EnumDefinition`) or, as a fallback, synthesized from the `OptionSetValues` property (`LocalizedText[]`). - -Impact on existing code: - -- **Source-breaking for custom `IComplexTypeBuilder` implementations**: a new member `AddOptionSetType(QualifiedName, ExpandedNodeId, ExpandedNodeId, ExpandedNodeId, ExpandedNodeId, EnumDefinition)` was added to `IComplexTypeBuilder`. Custom implementations must provide it. -- The Reflection.Emit builder in `Opc.Ua.Client.ComplexTypes` throws `NotSupportedException` from `AddOptionSetType`; callers relying on the Reflection.Emit path for OptionSet sub-types should switch to the default builder (`ComplexTypeSystem.Create(session, telemetry)`). -- No wire-format changes: encoders/decoders continue to route through `IEncodeableFactory` → `IEncodeableType.CreateInstance`, which now yields `Opc.Ua.Encoders.OptionSet` for registered sub-types. -- UInteger-backed OptionSet DataTypes remain treated as their underlying unsigned integer in a `Variant` (unchanged). +For session-bound loading, ownership, and OptionSet changes, see +[Complex Types](#complex-types) below. ## Encoders and Decoders @@ -74,7 +72,7 @@ behaviour is unchanged. Implement `IEncoder` directly instead of deriving from a The shared `ComplexTypeSystem` orchestrator, the complex type interfaces and the default (non-reflection-emit) type builder moved to the `Opc.Ua.Core.Schema` assembly under the root `Opc.Ua` namespace (which consumers already import) so they can be used by both client and server and existing code keeps compiling without adding a new `using`. Remove the old `Opc.Ua.Client.ComplexTypes` import if it is now unused (the client-only `NodeCacheResolver` and the `ComplexTypeSystem.Create(session, ...)` helpers stay in `Opc.Ua.Client.ComplexTypes`). The `ComplexTypeSystem(ISession, ...)` constructors were removed; construct a session-bound instance with `ComplexTypeSystem.Create(session, telemetry)` (the default, NativeAOT friendly builder) or `ComplexTypeSystem.Create(session, new ComplexTypeBuilderFactory(), telemetry)` for the Reflection.Emit builder. `ComplexTypeSystem` is now `IDisposable`: an instance from `Create` (or from `IComplexTypeSystemFactory.Create`) owns the `NodeCacheResolver` and its node cache, so dispose it once no more types are loaded through it; loaded types stay registered. -Servers build the same stand-ins for runtime-loaded DataTypes **by default** (`StandardServer.LoadComplexTypes`; opt out by setting it to `false`); configure the pass with `AddComplexTypeSystem()` or invoke `IServerInternal.LoadComplexTypesAsync(...)` directly. See `docs/ComplexTypes.md`. +Servers build the same stand-ins for runtime-loaded DataTypes **by default** (`StandardServer.LoadComplexTypes`; opt out by setting it to `false`). Configure complex-type loading with `AddComplexTypeSystem()`, or load the types directly with `IServerInternal.LoadComplexTypesAsync(...)`. See [Complex Types](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/ComplexTypes.md). ### OptionSet DataType support diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/identity.md b/.agents/skills/opcua-v20-migration/references/stack-migration/identity.md index 956f9719e7..66bff8141c 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/identity.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/identity.md @@ -2,104 +2,67 @@ > **When to read this:** Read this for the pluggable identity-provider model (`IClientIdentityProvider`, `IUserTokenAuthenticator`, `IAccessTokenProvider`, `ITokenIssuer`, `IIdentityClaims`), the new `IUserIdentityTokenHandler` registry, and the secret-store / caller-password registry. +## Contents + +- [User Identity Token Handlers](#user-identity-token-handlers) + - [Migrating from 1.5.x](#migrating-from-15x) + - [Earlier 2.0 previews](#earlier-20-previews) +- [User Identity Providers](#user-identity-providers) + - [`SessionManager.ImpersonateUser` → registry authenticators](#sessionmanagerimpersonateuser--registry-authenticators) + - [`ManagedSessionOptions.Identity` → `IdentityProvider`](#managedsessionoptionsidentity--identityprovider) +- [Secrets — caller-supplied passwords go through a secret registry](#secrets--caller-supplied-passwords-go-through-a-secret-registry) + ## User Identity Token Handlers -**Breaking Change**: Identity tokens no longer perform cryptographic -operations directly. The handler pattern introduced earlier is now -**fully asynchronous** and **non-disposable**, and the -`Certificate`-taking ctors of `UserIdentity` and -`X509IdentityTokenHandler` have been removed in favour of a -`CertificateIdentifier` + `ICertificateProvider` model that resolves -the private-key cert on demand. +### Migrating from 1.5.x -**Before**: +The wire token types are unchanged, but cryptographic operations now live on +`IUserIdentityTokenHandler` rather than on the generated identity tokens. +Most applications should use [identity providers](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/IdentityProviders.md) +and let session activation handle the token cryptography. + +For X.509 identities, replace direct certificate-based construction with an +identifier and the certificate manager's provider. Initialize the application +certificates before accessing `configuration.CertificateManager`: ```csharp - var token = new X509IdentityToken(); - using var handler = token.AsTokenHandler(); - handler.Encrypt(certificate, nonce, securityPolicy, context); - handler.Decrypt(certificate, nonce, securityPolicy, context); - var signature = handler.Sign(data, securityPolicy); - bool isValid = handler.Verify(data, signature, securityPolicy); - - using var userIdentity = new UserIdentity(certificate); // legacy ctor +UserIdentity userIdentity = await UserIdentity.CreateAsync( + certificateIdentifier, + passwordProvider, + configuration.CertificateManager.CertificateProvider, + ct); ``` -**After**: +For code that operates on raw tokens, obtain a handler with `token.AsTokenHandler()` +and await `EncryptAsync`, `DecryptAsync`, `SignAsync`, or `VerifyAsync`. Handlers +are not disposable. The current signatures, including optional certificate and +ephemeral-key arguments, are defined by +[`IUserIdentityTokenHandler`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/src/Opc.Ua.Core/Stack/Types/IUserIdentityTokenHandler.cs). +Do not replace the asynchronous operations with blocking waits. -```csharp - var token = new X509IdentityToken(); - var handler = token.AsTokenHandler(); // not IDisposable - await handler.EncryptAsync(certificate, nonce, securityPolicy, context, ct: ct); - await handler.DecryptAsync(certificate, nonce, securityPolicy, context, ct: ct); - SignatureData signature = await handler.SignAsync(data, securityPolicy, ct); - bool isValid = await handler.VerifyAsync(data, signature, securityPolicy, ct); +### Earlier 2.0 previews - // New cert-based UserIdentity: identifier + cache-aware provider. - UserIdentity userIdentity = await UserIdentity.CreateAsync( - certificateIdentifier, - passwordProvider, - configuration.CertificateManager.CertificateProvider, - ct); -``` +The following table applies only to applications that adopted earlier preview +handler/provider APIs. These are not additional APIs that 1.5.x consumers must +have used. -**New interface shape**: +| Earlier preview API | Current replacement | +| --- | --- | +| `IUserIdentityTokenHandler : IDisposable` | Drop `using` on handlers; the current interface is not disposable. | +| `UserIdentity : IDisposable` | Drop `using` on identities; manage certificate and secret ownership through their providers. | +| Synchronous `handler.Encrypt`, `Decrypt`, `Sign`, `Verify` | Await the corresponding `*Async` operation. | +| `new UserIdentity(Certificate)` | Use `UserIdentity.CreateAsync(certificateIdentifier, passwordProvider, certificateProvider, ct)`. | +| `new X509IdentityTokenHandler(Certificate)` | Use the `CertificateIdentifier`, `ICertificatePasswordProvider`, and `ICertificateProvider` constructor. | +| `UserIdentity.CreateAsync(certId, passwordProvider, telemetry, ct)` | Pass an `ICertificateProvider` instead of the telemetry argument. | -```csharp - public interface IUserIdentityTokenHandler : - ICloneable, IEquatable - { - UserIdentityToken Token { get; } - string DisplayName { get; } - UserTokenType TokenType { get; } +The current handler types are `AnonymousIdentityTokenHandler`, +`UserNameIdentityTokenHandler`, `X509IdentityTokenHandler`, and +`IssuedIdentityTokenHandler`. - void UpdatePolicy(UserTokenPolicy userTokenPolicy); - - ValueTask EncryptAsync( - Certificate receiverCertificate, byte[] receiverNonce, - string securityPolicyUri, IServiceMessageContext context, - ..., CancellationToken ct = default); - ValueTask DecryptAsync( - Certificate certificate, Nonce receiverNonce, - string securityPolicyUri, IServiceMessageContext context, - ..., CancellationToken ct = default); - ValueTask SignAsync( - byte[] dataToSign, string securityPolicyUri, - CancellationToken ct = default); - ValueTask VerifyAsync( - byte[] dataToVerify, SignatureData signatureData, - string securityPolicyUri, CancellationToken ct = default); - } -``` - -**Migration required**: - -| Removed | Replacement | -| ------- | ----------- | -| `IUserIdentityTokenHandler : IDisposable` | `IUserIdentityTokenHandler` (no `IDisposable`). Drop `using` on handler instances. Sensitive byte buffers (`UserNameIdentityTokenHandler.DecryptedPassword`, `IssuedIdentityTokenHandler.DecryptedTokenData`) are no longer cleared on disposal — secure-memory management is the secret store's responsibility (deferred to a future revision). | -| `UserIdentity : IDisposable`, `UserIdentity.Dispose()` | `UserIdentity` (no `IDisposable`). Drop `using` on `new UserIdentity(...)`. | -| `handler.Encrypt(...)` (sync) | `await handler.EncryptAsync(..., ct)` | -| `handler.Decrypt(...)` (sync) | `await handler.DecryptAsync(..., ct)` | -| `SignatureData handler.Sign(...)` (sync) | `await handler.SignAsync(..., ct)` | -| `bool handler.Verify(...)` (sync) | `await handler.VerifyAsync(..., ct)` | -| `new UserIdentity(Certificate)` (legacy ctor) | `await UserIdentity.CreateAsync(certificateIdentifier, passwordProvider, certificateProvider, ct)` — the new ctor stores the identifier; the cert is materialised on demand by the provider. | -| `new X509IdentityTokenHandler(Certificate)` | `new X509IdentityTokenHandler(CertificateIdentifier, ICertificatePasswordProvider, ICertificateProvider)` — handler holds no live Certificate; on `SignAsync` the provider's cache is consulted (`TryGetPrivateKeyCertificate`) then the store (`GetPrivateKeyCertificateAsync`). | -| `[Obsolete] new UserIdentity(CertificateIdentifier, CertificatePasswordProvider)` | `await UserIdentity.CreateAsync(certificateIdentifier, passwordProvider, certificateProvider, ct)` — the obsolete ctor blocked on async; the new factory does not pre-resolve. | -| `await UserIdentity.CreateAsync(certId, passwordProvider, telemetry, ct)` | `await UserIdentity.CreateAsync(certId, passwordProvider, certificateProvider, ct)` — `ICertificateProvider` (typically `configuration.CertificateManager.CertificateProvider`) replaces the telemetry-only argument list. | - -**Available token handlers** (all non-disposable): - - `AnonymousIdentityTokenHandler` - - `UserNameIdentityTokenHandler` - - `X509IdentityTokenHandler` - - `IssuedIdentityTokenHandler` - -**Note on secure-memory management**: with `IDisposable` gone, the -sync `Array.Clear` of decrypted password / issued-token bytes that -used to happen in `Dispose()` no longer fires. Bytes live in plain -fields until GC. A follow-up revision will route inbound decrypted -secrets through the new `ISecretStore` abstraction (see *Secrets* -below) so secure clearing becomes the store's responsibility, with no -public surface change. +**Sensitive-buffer lifetime:** handlers provide no disposal hook for clearing +server-side decrypted password or issued-token buffers. Do not log or retain +those buffers. The secret registry below manages caller-supplied secrets; it +does not promise secure clearing of every inbound decrypted token buffer. ## User Identity Providers diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/node-states.md b/.agents/skills/opcua-v20-migration/references/stack-migration/node-states.md index 2eef7dae82..ee70a66884 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/node-states.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/node-states.md @@ -2,6 +2,17 @@ > **When to read this:** Read this when migrating custom NodeManagers, `NodeState` clone / read / write helpers (`Clone` -> `CreateCopy`, removed `BaseVariableState` helpers), the new `INodeManager3` role-permission hooks, `OnAfterCreate(CancellationToken)`, predefined-node processing, generics on `BaseVariableState` / `BaseVariableTypeState`, code that took `lock (node)` on a `NodeState` or used `NodeBrowser.DataLock`, or `INodeCache.InvalidateNode`. +## Contents + +- [Node States](#node-states) + - [Generics and Typed BaseVariableState and BaseVariableTypeState](#generics-and-typed-basevariablestate-and-basevariabletypestate) + - [Predefined node processing](#predefined-node-processing) + - [NodeState Cloning and Lifecycle](#nodestate-cloning-and-lifecycle) + - [INodeManager3 - new role-permission and method-resolution hooks](#inodemanager3---new-role-permission-and-method-resolution-hooks) + - [NodeState guards itself; NodeBrowser is single-consumer (UA0027)](#nodestate-guards-itself-nodebrowser-is-single-consumer-ua0027) + - [NodeBrowser gains an async iteration seam](#nodebrowser-gains-an-async-iteration-seam) +- [`INodeCache` changes](#inodecache-changes) + ## Node States ### Generics and Typed BaseVariableState and BaseVariableTypeState @@ -10,7 +21,7 @@ With the changes to Variant, the generic node state classes reflecting the inner 1. T is a built in type -> use `VariantBuilder` 2. T is a instance of `IEncodeable` (a complex structure) -> Use `StructureBuilder` where T is the name of the structure. -3. T is an instance of Enum (an enumeration) -> Use `EnumBuilder` where T is the name fo the enumeration type. +3. T is an instance of Enum (an enumeration) -> Use `EnumBuilder` where T is the name of the enumeration type. E.g. to create an instance of a `PropertyState` where T is `ArrayOf` use @@ -391,7 +402,7 @@ Version 2.0 collapses the two parallel node-cache contracts into a single public | `FetchSuperTypesAsync(ExpandedNodeId, ct)` | extension method that loops `FindSuperTypeAsync`. | | `GetNodeWithBrowsePathAsync(NodeId, ArrayOf, ct)` | extension method on `NodeCacheExtensions`. | | `GetBuiltInTypeAsync(NodeId, ct)` | extension method on `NodeCacheExtensions`. | - | `GetDisplayTextAsync(INode | ExpandedNodeId | ReferenceDescription, ct)` | three extension methods on `NodeCacheExtensions`. | + | `GetDisplayTextAsync(INode \| ExpandedNodeId \| ReferenceDescription, ct)` | three extension methods on `NodeCacheExtensions`. | External implementations of `INodeCache` no longer need to implement these members. Call sites that already used `using Opc.Ua;` keep compiling unchanged because the extensions live in the same namespace. diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/packages.md b/.agents/skills/opcua-v20-migration/references/stack-migration/packages.md index b72a06f9c9..505febaeaa 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/packages.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/packages.md @@ -2,7 +2,16 @@ > **When to read this:** Read this for NuGet package renames / additions / removals, the new published packages, target-framework changes on `Opc.Ua.Types`, and the Newtonsoft.Json removal from `Opc.Ua.Core`. -### New published packages +## Contents + +- [New published packages](#new-published-packages) +- [Renamed packages — the GDS libraries drop `.Common`](#renamed-packages--the-gds-libraries-drop-common) +- [Target Frameworks (only Opc.Ua.Types changes)](#target-frameworks-only-opcuatypes-changes) +- [NuGet dependency additions and removals](#nuget-dependency-additions-and-removals) +- [ASP.NET Core packages are versioned per target framework](#aspnet-core-packages-are-versioned-per-target-framework) +- [Newtonsoft.Json - what really changed](#newtonsoftjson---what-really-changed) + +## New published packages The 2.0 packages are currently prereleases on nuget.org. Use `2.0.0-preview.*` to float to the latest published `2.0.0-preview.N` release, @@ -23,7 +32,7 @@ Two assemblies that previously shipped only as transitive content inside `Opc.Ua ``` -### Renamed packages — the GDS libraries drop `.Common` +## Renamed packages — the GDS libraries drop `.Common` The three GDS packages lose their `.Common` suffix, so the family matches every other companion specification in the stack (`Opc.Ua.Di`, `Opc.Ua.ISA95`, @@ -55,13 +64,20 @@ Assembly names follow the package ids (`Opc.Ua.Gds.Server.dll`, not `Opc.Ua.Gds.Server.Common.dll`), so update any binding redirects, ILMerge or trimming descriptors, or signing manifests that name the files directly. -### Target Frameworks (only Opc.Ua.Types changes) +## Target Frameworks (only Opc.Ua.Types changes) -The TFM matrix for the main libraries (Core, Client, Server, Configuration, etc.) is unchanged from 1.5.378: `net472;net48;netstandard2.1;net8.0;net9.0;net10.0`. The only consumer-visible change is the `Opc.Ua.Types` assembly: on 1.5.378 it tracked the dedicated `LibTypesTargetFrameworks` variable (`net472;net48;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0`); on 2.0 the variable is removed and `Opc.Ua.Types` tracks `LibCoreTargetFrameworks`, the same matrix as every other library. The net effect is that `netstandard2.0` is no longer offered for `Opc.Ua.Types`. +The main library target matrix includes `net472`, `net48`, `netstandard2.1`, +`net8.0`, `net9.0`, and `net10.0`. `Opc.Ua.Types` additionally targets +`netstandard2.0`: its project uses `$(LibCoreTargetFrameworks);netstandard2.0`. +A Types-only consumer does not need to retarget merely because it uses +`netstandard2.0`. Higher-level packages can have different requirements; check +the target assets of every package your application consumes. -The minimum SDK is the **.NET 10 SDK**, and projects compile with **`LangVersion 14.0`**. Projects that target `netstandard2.0` and pull in `Opc.Ua.Types` will fail to restore with `NU1202` ("package is not compatible") - retarget to `netstandard2.1` or one of the .NET / .NET Framework TFMs above. +Building this repository requires the **.NET 10 SDK** and **C# 14.0**. +Those build prerequisites are not the minimum runtime for every NuGet consumer. +See [platform support](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DeveloperGuide.md#packages-platform-support-and-versioning). -### NuGet dependency additions and removals +## NuGet dependency additions and removals | Package | Status in 2.0 | Referenced by | |---|---|---| @@ -88,18 +104,18 @@ The minimum SDK is the **.NET 10 SDK**, and projects compile with **`LangVersion | `System.Reflection.Metadata` 9.0.0 | Added (pinned) | Centralised pin only, no direct reference; tracks `$(RoslynRuntimeVersion)` for the analyzer closure | | `Mono.Options` 6.12.0.148 | Removed | Previously referenced by `samples/Reference/ConsoleReferenceServer/MonoReferenceServer.csproj` | -### ASP.NET Core packages are versioned per target framework +## ASP.NET Core packages are versioned per target framework `Microsoft.AspNetCore.Authentication.Certificate`, `Microsoft.AspNetCore.Authentication.JwtBearer`, `Microsoft.AspNetCore.Mvc.Testing` and `Microsoft.AspNetCore.TestHost` ship one band per .NET major and, unlike the `Microsoft.Extensions.*` packages, carry no `netstandard2.0` asset and do not roll forward across majors - a `net8.0` project cannot consume the `10.0.x` band. `Directory.Packages.props` therefore selects the version from `$(TargetFramework)`: `net8.0` gets `8.0.30`, `net9.0` gets `9.0.19`, and every other TFM (including `net10.0` and the `net10.0` shell that legacy `netstandard2.0`/`netstandard2.1` `$(CustomTestTarget)` builds fall back to) gets `10.0.11`. Consumers that pin these packages themselves are unaffected. Consumers that inherit them transitively through `Opc.Ua.Bindings.Https` receive the band matching their own target framework. -### Newtonsoft.Json - what really changed +## Newtonsoft.Json - what really changed -`Newtonsoft.Json` was removed as a direct dependency of `src/Opc.Ua.Core/Opc.Ua.Core.csproj` in 2.0. The only direct `` remaining anywhere under `src/` and `src/` is in `src/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj`. Consequences: - -- Consumers that reached `Newtonsoft.Json` only transitively through `Opc.Ua.Core` now need to add their own explicit reference. -- Consumers of `Opc.Ua.PubSub` continue to receive `Newtonsoft.Json` transitively and are unaffected. +Neither `Opc.Ua.Core` nor `Opc.Ua.PubSub` directly references `Newtonsoft.Json` +in 2.0. Consumers that used it only through either package's old dependency +chain must add an explicit reference if their own code still uses its API. +Do not depend on an unrelated package to provide it transitively. ```xml diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/pubsub.md b/.agents/skills/opcua-v20-migration/references/stack-migration/pubsub.md index f2dea840a9..d7e3473237 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/pubsub.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/pubsub.md @@ -11,12 +11,13 @@ required for existing consumers. ## Contents -1. [PubSub assemblies and NuGet packages renamed and split](#1-pubsub-assemblies-and-nuget-packages-renamed-and-split) -2. [`UaPubSubApplication.Create*` and the legacy 1.04 API are replaced](#2-uapubsubapplicationcreate-and-the-legacy-104-api-are-replaced) -3. [JSON encoder switched to System.Text.Json](#3-json-encoder-switched-to-systemtextjson) -4. [`JsonEncodingMode` Reversible/Non-Reversible encodings removed](#4-jsonencodingmode-reversiblenon-reversible-encodings-removed) -5. [UADP RawData bounded-field wire-compatibility break](#5-uadp-rawdata-bounded-field-wire-compatibility-break) -6. [Compatibility matrix](#6-compatibility-matrix) +- [1. PubSub assemblies and NuGet packages renamed and split](#1-pubsub-assemblies-and-nuget-packages-renamed-and-split) +- [2. `UaPubSubApplication.Create*` and the legacy 1.04 API are replaced](#2-uapubsubapplicationcreate-and-the-legacy-104-api-are-replaced) +- [3. JSON encoder switched to System.Text.Json](#3-json-encoder-switched-to-systemtextjson) +- [4. `JsonEncodingMode` Reversible/Non-Reversible encodings removed](#4-jsonencodingmode-reversiblenon-reversible-encodings-removed) +- [5. UADP RawData bounded-field wire-compatibility break](#5-uadp-rawdata-bounded-field-wire-compatibility-break) +- [6. Compatibility matrix](#6-compatibility-matrix) +- [See also](#see-also) ## 1. PubSub assemblies and NuGet packages renamed and split @@ -94,8 +95,9 @@ replaced with a `System.Text.Json`-backed encoder under `src/Opc.Ua.PubSub/Encoding/Json/`. Behaviour changes that may surface in callers: -- The `Newtonsoft.Json` dependency is dropped from the PubSub layer (it remains - transitively available via `Opc.Ua.Core` for legacy Variant JSON). +- The `Newtonsoft.Json` dependency is dropped. Core does not supply it either; + add an explicit reference if application code still uses Newtonsoft APIs. See + [package dependency migration](packages.md#newtonsoftjson---what-really-changed). - Numeric round-trips honour the .NET native precision instead of the Newtonsoft default (e.g. `double` → 17 significant digits, not 15). - The new encoder is `Utf8JsonWriter`-backed; allocations on the hot path drop diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/sessions-subscriptions.md b/.agents/skills/opcua-v20-migration/references/stack-migration/sessions-subscriptions.md index a4112a0dfb..f43440b3e0 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/sessions-subscriptions.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/sessions-subscriptions.md @@ -6,6 +6,27 @@ > subscriptions, removed `ReverseConnectClientCollection`, and transport > abstraction changes. +## Contents + +- [GDS Client API modernization](#gds-client-api-modernization) + - [`Task` → `ValueTask` on GDS client interfaces](#task--valuetask-on-gds-client-interfaces) + - [Removal of obsolete GDS APIs](#removal-of-obsolete-gds-apis) +- [ManagedSession and Automatic Reconnection](#managedsession-and-automatic-reconnection) + - [Configuring Reconnection Policy](#configuring-reconnection-policy) + - [Server Redundancy](#server-redundancy) + - [Service Call Behavior During Reconnect](#service-call-behavior-during-reconnect) + - [Fluent Builder, V2 Subscriptions, and Dependency Injection](#fluent-builder-v2-subscriptions-and-dependency-injection) +- [Server Session Activation and Subscription Transfer](#server-session-activation-and-subscription-transfer) +- [Subscriptions and Transports](#subscriptions-and-transports) + - [Durable subscriptions and reshaped Subscription tree](#durable-subscriptions-and-reshaped-subscription-tree) + - [Request completion is owned by `OperationContext`](#request-completion-is-owned-by-operationcontext) + - [`Opc.Ua.Server.ISession`: `IsClosing` and `InvalidateContinuationPoints`](#opcuaserverisession-isclosing-and-invalidatecontinuationpoints) + - [`Opc.Ua.Server.ISubscription`: the publish pipeline is server-internal](#opcuaserverisubscription-the-publish-pipeline-is-server-internal) + - [PubSub](#pubsub) + - [Reverse connect](#reverse-connect) + - [`IMessageSocket` abstraction removed](#imessagesocket-abstraction-removed) + - [Transport binding registry — `TransportBindings` static API removed](#transport-binding-registry--transportbindings-static-api-removed) + ## GDS Client API modernization The `Opc.Ua.Gds.Client` package has undergone a significant cleanup. Two breaking changes affect almost every consumer of the GDS / LDS / Server-Push client APIs. @@ -202,14 +223,14 @@ ManagedSession session = await new ManagedSessionBuilder(configuration, telemetr The V2 options-based subscription manager (`ISubscriptionManager`) is exposed on `ISession` through `bool TryGetSubscriptionManager(out ISubscriptionManager? manager)` — it returns `true` and the manager for V2-engine sessions (the default for `ManagedSession`) and `false` for classic-engine sessions. The classic `Subscriptions` property remains available alongside it. Use `UseSubscriptionEngine(ClassicSubscriptionEngineFactory.Instance)` on the builder if you need the legacy classic engine instead. > **Source-breaking (2.0 preview):** the earlier throwing `ManagedSession.SubscriptionManager` property has been **removed** in favor of `ISession.TryGetSubscriptionManager`. Replace `var manager = session.SubscriptionManager;` (which threw `InvalidOperationException` on classic-engine sessions) with: -> + > ```csharp > if (session.TryGetSubscriptionManager(out ISubscriptionManager? manager)) > { > // use manager (V2 engine) > } > ``` -> + > The `session.AddSubscription(...)` fluent extensions are unchanged. ```csharp @@ -393,8 +414,7 @@ already implements them. **Source-breaking for custom implementations and for callers.** This is the **server-side** `Opc.Ua.Server.ISubscription` — not the client-side `Opc.Ua.Client.Subscriptions.ISubscription` introduced by the V2 subscription shape above. Twelve members leave the interface -(analyzer `UA0030`; see [MigrationGuide.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/MigrationGuide.md#ua0030) for the -caller-side migration): +(analyzer `UA0030`): - `ItemReadyToPublish` and `ItemNotificationsAvailable` are **deleted**. Their implementation bodies had been commented out since 1.5.x, so every call was a no-op — delete the calls and @@ -427,13 +447,22 @@ rest of the pipeline members. queue is the other half of the publish protocol. Code that constructed or drove one should use the `Publish` service path via `ISubscriptionManager.PublishAsync`. +For the public operations that remain, obtain a subscription with +`ISubscriptionManager.TryGetSubscription` before calling `ResendData`, +`GetMonitoredItems`, or the monitored-item operations. Acknowledgements belong +in the Publish request, not in direct calls into the subscription's internal +publishing state machine. + ### PubSub -**Not source-breaking.** No public top-level types in `Opc.Ua.PubSub` were removed or renamed in 2.0. Changes are limited to internal modernization, AOT preparation, and diagnostics improvements. `Newtonsoft.Json` remains a direct `` of `src/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj`, so PubSub consumers keep receiving it transitively (see [Newtonsoft.Json - what really changed](#newtonsoftjson---what-really-changed)). +PubSub has source-breaking package and API changes. See the +[PubSub migration guide](pubsub.md) for removed application/connection types, +builder replacements, JSON encoding changes, and wire-compatibility limits. +Dependency changes are maintained in the [package guide](packages.md#newtonsoftjson---what-really-changed). ### Reverse connect -**Not source-breaking.** `ReverseConnectManager`, `ReverseConnectProperty`, and `ReverseConnectServer` retain the same public shape in 2.0. The previously published `ReverseConnectClientCollection` wrapper has been removed; this is already covered by the broader [Configuration collection types removed](#configuration-collection-types-removed) guidance. +`ReverseConnectManager`, `ReverseConnectProperty`, and `ReverseConnectServer` retain the same public shape in 2.0. The `ReverseConnectClientCollection` wrapper is removed, which is source-breaking for callers using that collection. See [Configuration collection types removed](types.md#configuration-collection-types-removed). ### `IMessageSocket` abstraction removed diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/source-generation.md b/.agents/skills/opcua-v20-migration/references/stack-migration/source-generation.md index 03187df4e8..cf699a7ded 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/source-generation.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/source-generation.md @@ -2,7 +2,17 @@ > **When to read this:** Read this for the move from pre-generated code files to the source-generated NodeManager / data-type model, including the project-structure changes and the new default for boolean properties. -Instead of generating code for OPC UA design files using the [ModelCompiler](https://github.com/OPCFoundation/UA-ModelCompiler), this version of the stack uses [Source Generators](https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/#source-generators) to generate code behind for your project. Input into the source generator can be NodeSet2.xml files or ModelDesign.xml files (the same that ModelCompiler consumes). Example projects are provided in the Applications folder. Source generators are Roslyn analyzers, that are called by the Roslyn compiler and emit code during the build process. +## Contents + +- [Migrating model projects](#migrating-model-projects) +- [Default value of boolean properties in source-generated data types is now false](#default-value-of-boolean-properties-in-source-generated-data-types-is-now-false) +- [Server default Aggregate configuration now treats Uncertain as Bad (Part 13)](#server-default-aggregate-configuration-now-treats-uncertain-as-bad-part-13) +- [MIG01 resolution playbook](#mig01-resolution-playbook) +- [Project Structure](#project-structure) + +## Migrating model projects + +Instead of generating code for OPC UA design files using the [ModelCompiler](https://github.com/OPCFoundation/UA-ModelCompiler), this version of the stack uses [Source Generators](https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/#source-generators) to generate code behind for your project. Input into the source generator can be NodeSet2.xml files or ModelDesign.xml files (the same that ModelCompiler consumes). Example projects are provided under `samples/`. Source generators are Roslyn analyzers, that are called by the Roslyn compiler and emit code during the build process. **Model compiler generated csharp code is not supported in this version!** @@ -30,9 +40,9 @@ Code generation during compilation also allows not just emitting code ahead of t The stack itself uses source generators to generate the core opc ua code. Therefore all pre-generated code files (`Generated/` folders) have been removed and are now generated at build time. As a result of using source generators to generate the stack code all `*.nodeset2.xml` files previously included as embedded zip have been removed. Also, all `*.Types.xsd` and `*.Types.bsd` files are now included as string resource instead of embedded resources. If you need access to these, use the new `Schemas.XmlAsStream` and `Schemas.BinaryAsStream` APIs in the node manager namespace which produce a utf8 stream. Alternatively you can use the existing ModelCompiler tool to generate these files. -When you encounter slower build times use incremental compilation and avoid changes to code in Opc.Ua and Opc.Ua.Core project. In addition you can change your builds to only build for your target framework using the dotnet `-f ` command line option, e.g. `-f net10`. +When you encounter slower build times use incremental compilation and avoid changes to code in Opc.Ua and Opc.Ua.Core project. In addition you can change your builds to only build for your target framework using the dotnet `-f ` command line option, e.g. `-f net10.0`. -### Default value of boolean properties in source-generated data types is now false +## Default value of boolean properties in source-generated data types is now false **Breaking Change**: Boolean properties on source-generated data types now correctly default to `false` instead of `true`. @@ -70,7 +80,7 @@ var connection = new PubSubConnectionDataType }; ``` -### Server default Aggregate configuration now treats Uncertain as Bad (Part 13) +## Server default Aggregate configuration now treats Uncertain as Bad (Part 13) **Behavioral Change (Part 13 compliance)**: The server-side default aggregate configuration returned by `AggregateManager.GetDefaultConfiguration(...)` — used when a `ReadProcessedDetails` request sets @@ -83,7 +93,7 @@ Uncertain-quality samples as Bad when computing aggregate `StatusCode`s (unless definition states otherwise). Clients that require the previous behavior should send an explicit `AggregateConfiguration` with `TreatUncertainAsBad = false` instead of `UseServerCapabilitiesDefaults = true`. -### MIG01 resolution playbook +## MIG01 resolution playbook The migration analyzer's source generator emits temporary `Collection : List` compatibility wrappers. It reports `MIG01` when @@ -106,7 +116,7 @@ replacing the wrapper with the intended fully qualified `List` or wrapper explicitly in consumer source so the unresolved reference binds and the generator does not try to synthesize it. -### Project Structure +## Project Structure New `Opc.Ua` project as an intermediate project. Impact: diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/telemetry.md b/.agents/skills/opcua-v20-migration/references/stack-migration/telemetry.md index 6eb764fabd..fcb23914bb 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/telemetry.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/telemetry.md @@ -35,6 +35,29 @@ ISession session = await factory.CreateAsync(/* ... */); --- +## Contents + +- [Replacing the static logger surface](#replacing-the-static-logger-surface) + - [From static logger management](#from-static-logger-management) + - [From static `Utils.Log*` methods](#from-static-utilslog-methods) + - [From `Utils.Trace*` methods](#from-utilstrace-methods) +- [Non-exhaustive summary of breaking changes](#non-exhaustive-summary-of-breaking-changes) + - [Core infrastructure constructors](#core-infrastructure-constructors) + - [Interface additions](#interface-additions) + - [System context](#system-context) + - [Certificate and security](#certificate-and-security) + - [Transport layer](#transport-layer) + - [Client library](#client-library) + - [Configuration library](#configuration-library) + - [Server library](#server-library) + - [PubSub library](#pubsub-library) + - [Method signatures](#method-signatures) + - [Removed and `[Obsolete]` `Utils` static APIs](#removed-and-obsolete-utils-static-apis) +- [ETW `EventSource` provider removal](#etw-eventsource-provider-removal) + - [`ClientTraceFlags.EventLog` removed (source breaking)](#clienttraceflagseventlog-removed-source-breaking) + - [Migrating `EventListener` / `dotnet-trace` consumers](#migrating-eventlistener--dotnet-trace-consumers) +- [Migration utilities](#migration-utilities) + ## Replacing the static logger surface The legacy static logger management (`Utils.SetLogger` / @@ -108,9 +131,9 @@ explicit telemetry context**. ### Core infrastructure constructors -- `ServiceMessageContext()` → `ServiceMessageContext(ITelemetryContext)` +- `ServiceMessageContext()` → `ServiceMessageContext(ITelemetryContext)` (parameter-less constructor removed). -- `EncodeableFactory()` → `EncodeableFactory(ITelemetryContext)` +- `EncodeableFactory()` → `EncodeableFactory(ITelemetryContext)` (parameter-less constructor removed). - `ApplicationConfiguration` gains `ApplicationConfiguration(ITelemetryContext)`. @@ -128,7 +151,7 @@ explicit telemetry context**. ### System context -- `SystemContext()` → `SystemContext(ITelemetryContext)` and +- `SystemContext()` → `SystemContext(ITelemetryContext)` and `SystemContext(IOperationContext, ITelemetryContext)` (parameter-less constructor removed). @@ -137,9 +160,9 @@ explicit telemetry context**. - `CertificateValidator` gains `CertificateValidator(ITelemetryContext)`. - `CertificateStoreIdentifier.CreateStore(string, ITelemetryContext)` - and `OpenStore(ITelemetryContext)` — custom stores must + and `OpenStore(ITelemetryContext)` — custom stores must implement the new signature. -- `CertificateIdentifier.OpenStore(ITelemetryContext)` — same +- `CertificateIdentifier.OpenStore(ITelemetryContext)` — same custom-store impact. - `CertificateIdentifierCollection` no longer implements `ICertificateStore`; use `CertificateIdentifierCollectionStore` @@ -197,10 +220,10 @@ explicit telemetry context**. ### Method signatures - `UserIdentityToken.GetOrCreateCertificate(ITelemetryContext)` - — certificate creation is no longer implicit on the + — certificate creation is no longer implicit on the `Certificate` getter; call this method explicitly when needed. - `ServerSecurityPolicy.CalculateSecurityLevel(..., ILogger)` - — the non-logger overload is `[Obsolete]`. + — the non-logger overload is `[Obsolete]`. ### Removed and `[Obsolete]` `Utils` static APIs @@ -244,7 +267,7 @@ The stack shipped four internal `EventSource` providers for high-performance tra | `OPC-UA-Server` | `Opc.Ua.Server` | `OPC-UA-Server` | | `Opc.Ua.ChannelManager` | `Opc.Ua.Core` (client channel manager) | `Opc.Ua.ChannelManager` | -The replacement logger category is always the **exact old ETW provider name**, not the assembly's usual typed category. Migrated filters can therefore keep the same identifying string (for example, `AddFilter("OPC-UA-Client", LogLevel.Trace)`) after they move from an ETW provider subscription to `ILogger` configuration. `EventLevel` mapped to `LogLevel` on a like-for-like basis (`Verbose` → `Trace`, `Informational` → `Information`, `Warning` → `Warning`, `Error`/`Critical` → `Error`/`Critical`). See [DeveloperGuide.md — narrow exception: retained EventSource-compatibility ids](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) for the authoring-side rules and [Sessions.md — diagnostics surface contract](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) for the full `Opc.Ua.ChannelManager` event table. +The replacement logger category is always the **exact old ETW provider name**, not the assembly's usual typed category. Migrated filters can therefore keep the same identifying string (for example, `AddFilter("OPC-UA-Client", LogLevel.Trace)`) after they move from an ETW provider subscription to `ILogger` configuration. `EventLevel` mapped to `LogLevel` on a like-for-like basis (`Verbose` → `Trace`, `Informational` → `Information`, `Warning` → `Warning`, `Error`/`Critical` → `Error`/`Critical`). See [DeveloperGuide.md — narrow exception: retained EventSource-compatibility ids](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) for the authoring-side rules and [Sessions.md — diagnostics surface contract](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) for the full `Opc.Ua.ChannelManager` event table. The `OPC-UA-Server` compatibility category remains opt-in like the retired EventSource provider. Enable that category at `Trace` to receive its records, including the `ServerCall` and `SessionState` records whose retained legacy level is `Information`. A global `Information` minimum therefore does not emit a record for every server request. @@ -311,15 +334,15 @@ builder.Services.AddOpenTelemetry().WithLogging(l => l To aid migration the stack provides: -- `DefaultTelemetry.Create(...)` — convenient factory that +- `DefaultTelemetry.Create(...)` — convenient factory that returns an `ITelemetryContext` backed by the trace logger when no configuration is supplied. -- `Telemetry.NullLogger` / `Telemetry.NullLogger` — +- `Telemetry.NullLogger` / `Telemetry.NullLogger` — no-op-in-release / debug-check-in-debug logger you can assign to an `m_logger` field to avoid null-reference exceptions while a class is being migrated. Distinct from `NullLogger.Instance` in `Microsoft.Extensions.Logging.Abstractions`. -- `Utils.Fallback.Logger` — an `ILogger` that mimics the +- `Utils.Fallback.Logger` — an `ILogger` that mimics the legacy static `ILogger`. Use as a strictly temporary placeholder in places where no telemetry context can yet be plumbed through. The `Fallback` class is marked `Experimental` and may be removed @@ -334,10 +357,10 @@ test time. **See also** - Related: [packages.md](packages.md), [configuration.md](configuration.md). -- [`docs/Diagnostics.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/Diagnostics.md) — full +- [`docs/Diagnostics.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/Diagnostics.md) — full end-state usage and extensibility guidance for `ITelemetryContext` (custom contexts, OpenTelemetry wiring, metrics inventory). -- [`docs/Sessions.md` — diagnostics surface contract](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) — full `Opc.Ua.ChannelManager` compatibility event table and safe-field policy. -- [`docs/DeveloperGuide.md` — narrow exception: retained EventSource-compatibility ids](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) — authoring rules for the retained compatibility ids. -- [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table. -- [Migration Guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/MigrationGuide.md) — landing page across versions. +- [`docs/Sessions.md` — diagnostics surface contract](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) — full `Opc.Ua.ChannelManager` compatibility event table and safe-field policy. +- [`docs/DeveloperGuide.md` — narrow exception: retained EventSource-compatibility ids](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) — authoring rules for the retained compatibility ids. +- [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table. +- [Migration Guide](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/MigrationGuide.md) — landing page across versions. diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/timeprovider.md b/.agents/skills/opcua-v20-migration/references/stack-migration/timeprovider.md index 6593364061..54cfb74f61 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/timeprovider.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/timeprovider.md @@ -2,6 +2,13 @@ > **When to read this:** Read this when adopting `System.TimeProvider` across the stack (replacing direct `DateTime.UtcNow`, `Timer`, and similar timing primitives in custom NodeManagers, durable subscriptions, and reconnect policies). +## Contents + +- [Adopting TimeProvider](#adopting-timeprovider) +- [Monotonic timestamps for duration calculations](#monotonic-timestamps-for-duration-calculations) + +## Adopting TimeProvider + **Not source-breaking.** The stack now uses [`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider) as its canonical clock and scheduler so that timeouts, intervals, keep-alive loops, @@ -99,7 +106,7 @@ The `Timer` field type changes from `System.Threading.Timer` to `ITimer` — bot implement `IDisposable` and the same `Change` / `Dispose` semantics; only the parameter types on `Change` differ (`TimeSpan` instead of `int`/`uint`/`long`). -### Monotonic timestamps for duration calculations +## Monotonic timestamps for duration calculations `TimeProvider.GetTimestamp()` returns a `long` monotonic timestamp that does not suffer from the 32-bit wraparound of `Environment.TickCount` / `HiResClock.TickCount` diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/transport-listener-async.md b/.agents/skills/opcua-v20-migration/references/stack-migration/transport-listener-async.md index 75fc2b0164..85bd3d6e1d 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/transport-listener-async.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/transport-listener-async.md @@ -11,6 +11,23 @@ There is **no `[Obsolete]` shim** for this change; the sync methods are gone. Update every call site that previously invoked `listener.Open` / `listener.Close` / `listener.Dispose`. +## Contents + +- [What changed](#what-changed) + - [`ITransportListener`](#itransportlistener) + - [`ITransportListenerCertificateRotation`](#itransportlistenercertificaterotation) + - [`ITransportListenerFactory`](#itransportlistenerfactory) + - [`ServerBase`](#serverbase) + - [`ReverseConnectHost`](#reverseconnecthost) + - [`ReverseConnectManager.RegisterWaitingConnection`](#reverseconnectmanagerregisterwaitingconnection) + - [Reverse-connect configuration providers](#reverse-connect-configuration-providers) + - [Dependency-injection startup](#dependency-injection-startup) +- [Migration steps](#migration-steps) + - [Application / sample code](#application--sample-code) + - [Custom transport binding implementations](#custom-transport-binding-implementations) + - [Custom server subclass (`StandardServer` / `LdsServer` heir)](#custom-server-subclass-standardserver--ldsserver-heir) +- [Why](#why) + ## What changed ### `ITransportListener` diff --git a/.agents/skills/opcua-v20-migration/references/stack-migration/types.md b/.agents/skills/opcua-v20-migration/references/stack-migration/types.md index 9399c64bbe..49abc6f203 100644 --- a/.agents/skills/opcua-v20-migration/references/stack-migration/types.md +++ b/.agents/skills/opcua-v20-migration/references/stack-migration/types.md @@ -2,7 +2,29 @@ > **When to read this:** Read this when hit by `CS0029` / `CS1503` / `CS0266` on `NodeId`, `Variant`, `DataValue`, `ExtensionObject`, `QualifiedName`, `LocalizedText`, `ArrayOf` / `MatrixOf`, `ByteString`, `StatusCode`, `XmlElement`, `EnumValue`, or by `[Obsolete]` warnings on built-in type APIs - covers every value-type and `Variant`-for-`object` migration. Maps to analyzer rules `UA0002`–`UA0008`, `UA0014`, and `UA0019`. -### Several built in types are now immutable value types +## Contents + +- [Several built in types are now immutable value types](#several-built-in-types-are-now-immutable-value-types) +- [ByteString](#bytestring) +- [ArrayOf and MatrixOf](#arrayof-and-matrixof) + - [Configuration collection types removed](#configuration-collection-types-removed) + - [Generated data type fields with ValueRank=OneOrMoreDimensions](#generated-data-type-fields-with-valuerankoneormoredimensions) +- [DateTimeUtc](#datetimeutc) +- [QualifiedName and LocalizedText](#qualifiedname-and-localizedtext) +- [StatusCode](#statuscode) +- [NodeId/ExpandedNodeId](#nodeidexpandednodeid) +- [Variant, DataValue and ExtensionObject](#variant-datavalue-and-extensionobject) + - [Deprecated boxing behavior](#deprecated-boxing-behavior) + - [Replacement of all use of System.Object in generated code and API](#replacement-of-all-use-of-systemobject-in-generated-code-and-api) +- [DataValue](#datavalue) +- [XmlElement](#xmlelement) +- [EnumValue to represent the enumeration built in type](#enumvalue-to-represent-the-enumeration-built-in-type) +- [ExtensionObject array helpers changed](#extensionobject-array-helpers-changed) +- [Other Data Types](#other-data-types) +- [Obsoleted APIs and replacements](#obsoleted-apis-and-replacements) +- [APIs permanently removed](#apis-permanently-removed) + +## Several built in types are now immutable value types The `Variant` and `TypeInfo`, `NodeId`, `ExpandedNodeId`, `ExtensionObject`, `LocalizedText` and `QualifiedName` are now `readonly struct`s. This is a large breaking change and affects existing usage: @@ -10,7 +32,7 @@ The `Variant` and `TypeInfo`, `NodeId`, `ExpandedNodeId`, `ExtensionObject`, `Lo 2. The default item can be created by assigning `default`, e.g. producing `NodeId.Null` for NodeId and `QualifiedName.Null` for QualifiedName. It is recommended to use the `Null` property on these types for readability and per your coding conventions. 3. Any API that mutated an instance of one of these built in types must be replaced with methods that return a new value of the type, e.g. `NodeId.WithNamespaceIndex(ushort)` as setters were removed. -### ByteString +## ByteString Previously the OPC UA built-in type *ByteString* was represented as `byte[]`. This caused ambiguities with regards to it and the byte *array* type. This has changed and `ByteString` is now a type in the Opc.Ua namespace. It is a wrapper around `ReadOnlyMemory` and while `Variant` handles both still interchangeably, the generated API now simplifies mixing of byte arrays and `ByteString` without confusion. @@ -29,7 +51,7 @@ To migrate, perform the following general replacements in your code: - If your code tried to set a byte in the ByteString, create a buffer `byte[]` and after changing convert to `ByteString` using `ByteString.From(buffer)` or `.ToByteString()` extension method - Perform changes only where you encounter build breaks. This should be enough to get into a working state. Later adjust the code as needed. -### ArrayOf and MatrixOf +## ArrayOf and MatrixOf Similar to `ByteString`, `ArrayOf` and `MatrixOf` are new type safe and sliceable generic value types representing non-scalar values. They are immutable meaning the values at an index inside them cannot be "set" unless they are converted to a `Span` (and then reconverted to a `ArrayOf`/`MatrixOf`). @@ -74,11 +96,11 @@ Note that equality operators and methods now compare the content of the Array an ArrayOf i = c.ConvertAll(v => (int)v); ``` -#### Configuration collection types removed +### Configuration collection types removed All `List`-based collection wrappers for configuration types have been removed and replaced with `ArrayOf`: `ServerSecurityPolicyCollection`, `TransportConfigurationCollection`, `SamplingRateGroupCollection`, `ReverseConnectClientCollection`, `ReverseConnectClientEndpointCollection`, `ServerRegistrationCollection`, `CertificateIdentifierCollection`, `CertificateGroupConfigurationCollection`, `OAuth2ServerSettingsCollection`, `OAuth2CredentialCollection`. -#### Generated data type fields with ValueRank=OneOrMoreDimensions +### Generated data type fields with ValueRank=OneOrMoreDimensions Previously, every structure field declared with `ValueRank="OneOrMoreDimensions"` in a model design was generated as `global::Opc.Ua.Variant`. The property is now typed as `global::Opc.Ua.MatrixOf` (mirroring the `ArrayOf` treatment already used for `ValueRank="Array"`). Encoding/decoding still flows through `Variant`, but the boxing/unboxing happens inside the encoder calls so consumers see the typed surface. @@ -115,7 +137,7 @@ The element type follows the field's `DataType`: - `IDecoder` gained a parameterless `ReadEncodeableMatrix(string? fieldName) where T : IEncodeable, new()` overload that mirrors the existing `ReadEncodeableArray(string? fieldName)` shape. Custom `IDecoder` implementations should add this overload alongside the existing encoding-id variant. -##### VariableType State classes, PropertyState instances, and service parameters +#### VariableType State classes, PropertyState instances, and service parameters The same `MatrixOf` opt-in now extends beyond structure data type fields to three sibling sites in the source generator: @@ -148,7 +170,7 @@ For *concrete* matrix variable types (today only `XYArrayItemType`) and matrix-r }.ToMatrixOf(2); ``` -### DateTimeUtc +## DateTimeUtc Previously the **DateTime** built in type was represented by the `System.DateTime` type. It is now represented by the `Opc.Ua.DateTimeUtc` type. This new type complies with the details of the spec without requiring external helper methods to be used. It's Value property returns the ticks, bounded by the information in Part 6 of the spec, and its time is always UTC. There are conversion operations to and from `DateTime`, but also `DateTimeOffset` and `long` and a minimal subset of `System.DateTime` API to allow for simpler porting. `DateTime` implicitly converts to `DateTimeUtc`, but not vice versa to force use of the new type. @@ -158,11 +180,11 @@ Previously the **DateTime** built in type was represented by the `System.DateTim - Replace `DateTime.UtcNow` with `DateTimeUtc.Now` for UTC time "right now". `DateTime.Now` or `DateTime.Today` can be cast or replaced with its Utc variant, which is likely intended anyway as all date/time values in OPC UA are UTC. - When assigning a `DateTime` value to a `DateTimeUtc` variable, add a cast, or use the corresponding `DateTimeUtc` constructor. -### QualifiedName and LocalizedText +## QualifiedName and LocalizedText There is no implicit conversion from `string` to `QualifiedName` or `LocalizedText` anymore. For one, it flags areas where null assignment is happening implicitly, and secondly, it makes the API more explicit. E.g. previously it was possible to assign a string to a browse name which landed the browse name accidentally in namespace 0 instead of the owning namespace. If you know what you are doing you can explicitly cast the string, but it is suggested to use the new static `From` API instead. -### StatusCode +## StatusCode `StatusCode` contains now not only a uint code, but also a symbol. Symbols are interned strings and using the `StatusCodes` constants therefore come with the symbol string. This removes the need to look up the symbolic id, however, when receiving a uint code it needs to be translated to a StatusCode constant to retain the Symbol. Older API has been obsoleted with proper instructions. Since types are immutable it is important to replace mutation calls with the proper replacement method and store the returned value. @@ -180,7 +202,7 @@ A matching `GetHashCode(StatusCodeComparison comparison)` overload is available When migrating code that compared `statusCode.Code` (the raw `uint`) against a `StatusCodes.XXX` constant, prefer the `==` operator (`statusCode == StatusCodes.XXX`) so the comparison ignores the non-code bits. Likewise replace comparisons against `statusCode.CodeBits` with the `==` operator. -### NodeId/ExpandedNodeId +## NodeId/ExpandedNodeId `NodeId`s with integer identifiers (the most common case) now do not box the integer identifier anymore into an object, making the entire NodeId heap allocation free (*). ExpandedNodeId with integer identifiers only contain an allocated namespace Uri, which is mostly a const (interned) string, reducing small allocations across both types. Because both types are now immutable, they must be mutated using the provided `With`. Access to the identifier in boxed form (object) is deprecated. Instead use the `TryGetValue(out uint/string/Guid/byte[])` API. If you need to get the identifier only to "stringify" it, use the `IdentifierAsText` property which avoids boxing integer identifiers. @@ -188,7 +210,7 @@ There is no implicit conversion from `uint`/`Guid`/`string`/`byte[]` to `NodeId` > (*) Note that NodeId leverages the new `uint` field to cache the HashCode of a "non-uint" "Identifier", which provides faster lookup using NodeId/ExpandedNodeId as key. -### Variant, DataValue and ExtensionObject +## Variant, DataValue and ExtensionObject Previously the `Variant` was a *mutable* struct containing a `TypeInfo` and `Value` property allowing setting the inner state and returning `object`. All value types thus were implicitly boxed to object and landing on the heap. The new `Variant` only boxes value types > 8 bytes in size (*), and stores the rest in a union. `TypeInfo`, previously a class, also now is stored as a 4 byte type (with padding). @@ -209,7 +231,7 @@ var output = session.Call(objectId, methodId, `null` arguments must be passed as `Variant.Null` (a literal `null` will not bind to the `params Variant[]` overload). -#### Deprecated boxing behavior +### Deprecated boxing behavior Access to the `Value` property of `Variant` is marked as [Obsolete] to discourage use in favor of casting to `` or `Get()` (both throw) or preferably `bool TryGetValue(out value)` calls. The same applies to the `Value` property of `DataValue`. The APIs perform any required conversion between `BuiltInType.Int32` and `BuiltInType.Enumeration` as well as arrays of `BuiltInType.Byte` and `BuiltInType.ByteString`. This also applies to the `Body` property of `ExtensionObject`. Here prefer the use of `TryGetValue` and `TryGetBinary, TryGetJson, TryGetXml`. @@ -223,7 +245,7 @@ To perform conversion from `` to a Variant, helper methods are available in ` > All other built in value types (`ExtensionObject`, `NodeId`, `QualifiedName`, `LocalizedText`, `Uuid`, etc.) are > 8 bytes in size and are therefore boxed when stored inside a Variant. > Future improvements will make certain types like `ArrayOf` be stored *spliced* inside the Variant (where the array pointer is stored in the object, and length/offset inside the union). -#### Replacement of all use of System.Object in generated code and API +### Replacement of all use of System.Object in generated code and API `Variant` is now the type reflecting the OPC UA Variant type in all API. That means all generated API now uses Variant instead of `System.Object` and all `Value` Properties are `Variant` too. This provides type safety and removes the need for Reflection via `GetType()` when the underlying type already is `Variant`. @@ -252,7 +274,7 @@ To migrate, perform the following general replacements in your code: - For Variable and VariableType node state classes that provide a narrowed "Value" via generic `` any access to `T Value` incurs a heavy type check. It is recommended to use `WrappedValue` instead when possible for assignment and access. - While most assignments work implicitly, use `TypeInfo.GetDefaultVariantValue` instead of `TypeInfo.GetDefaultValue` to initialize a variant value to a default that is `!= Variant.Null`. -### DataValue +## DataValue `DataValue` has been converted from a reference type (class) to a `readonly struct` to relieve GC pressure on hot subscription/encoder paths. The semantics are aligned with the other immutable built-in types (`NodeId`, `ExtensionObject`, etc.). @@ -310,13 +332,13 @@ async Task EnqueueAsync(DataValue dv) } ``` -### XmlElement +## XmlElement Previously the `XmlElement` built in type was represented by the `System.Xml.XmlElement` system type. While officially a deprecated, there is now a value type `XmlElement` that merely wraps a string but provides conversion operations to `System.Xml.XmlElement` and `System.Linq.Xml.XNode` as well as validation and equality/hashing operations. Normally you just need to remove `using System.Xml` and code continues working as is. If you need to have access to the `System.Xml.XmlElement` cast or use the `ToXmlElement` method. > `XmlElement` types are compared via a normalized version of the XML `string` contained, which removes all whitespace before comparing. This can result in some ambiguity, but operates well enough for test operations. For complete equality, cast to XNode and use `DeepEquals`. -### EnumValue to represent the enumeration built in type +## EnumValue to represent the enumeration built in type `EnumValue` bundles a symbol with a integer value (same as `StatusCode`). While most API works with standard .net `enum` types, these do not work in scenarios where the enum value is the result of a `EnumDefinition`. For these cases the `EnumValue` overloads provide a similar experience to using `enum`. In addition, the `EnumValue` type @@ -333,11 +355,11 @@ Variant v = new Variant(EnumValue.From(MyEnum.Value)); // or Variant v = Variant.From(MyEnum.Value); ``` -### ExtensionObject array helpers changed +## ExtensionObject array helpers changed `ExtensionObject.ToArray(object, Type)` and `ToList(object)` removed. Use `extensionObjects.GetStructuresOf()` or `ExtensionObject.ToArray(ArrayOf)`. -### Other Data Types +## Other Data Types All generated data types implementing `IEncodeable` are now equality comparable using `==` and `!=` and implement `IEquatable`. Equality defaults to the `IsEqual` implementation of the `IEncodeable` interface. In addition `ToString()` and `GetHashCode()` are implemented making all generated data types effectively equivalent to `record` classes with the exception of supporting `with` expressions. @@ -348,7 +370,7 @@ No changes are required, however there can be subtle bugs exposed, e.g.: - When comparing data type instances for reference equality, use `ReferenceEquals`, instead of `==` or `!=` operators. You can use the `RefEqualityComparer` helper when creating Dictionaries that use the type as key and require reference equality semantics for it. - When testing for `null`, use `is null` for more performant code. -### Obsoleted APIs and replacements +## Obsoleted APIs and replacements - `NodeId(string text)` -> `NodeId.Parse(string)` - `NodeId(object identifier, ushort namespaceIndex)` -> typed constructors: `new NodeId(uint, ushort)`, `new NodeId(Guid, ushort)`, `new NodeId(string, ushort)`, `new NodeId(ByteString, ushort)` @@ -376,7 +398,7 @@ No changes are required, however there can be subtle bugs exposed, e.g.: - `new DataValue(StatusCode)` and `new DataValue(StatusCode, DateTimeUtc)` -> use `DataValue.FromStatusCode(StatusCode)` and `DataValue.FromStatusCode(StatusCode, DateTimeUtc)`. The constructors suffered from a C# overload resolution bug where `new DataValue(42)` silently resolved to `DataValue(StatusCode)` instead of `DataValue(Variant)`, losing the value. - `SessionManager.ImpersonateUser` -> register `IUserTokenAuthenticator` instances via `services.AddIdentityAuthenticator()` or `server.CurrentInstance.IdentityRegistry.Register(...)`. The event remains functional as a fallback, but is now `[Obsolete]`; the in-box ReferenceServer, GlobalDiscoverySampleServer, and ConsoleReferenceClient samples use the provider model. -### APIs permanently removed +## APIs permanently removed - All `Collection` classes, e.g. Int32Collection or ArgumentCollection -> use `List` or `ArrayOf` - `ICloneable`/`Clone()`/`MemberwiseClone()` on the immutable built-in types -> use assignment for copies diff --git a/README.md b/README.md index 68a428cdfc..d5a9a5dd75 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,11 @@ For the full feature breakdown see ## 🚀 Getting started +To run a first OPC UA client and server, from NuGet packages or from the +samples in this repository, follow **[Getting started](docs/GettingStarted.md)**. +The **[documentation index](docs/README.md)** then leads from the core concepts +to advanced and experimental features. + You need the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) to build the repo. From the repository root: @@ -84,7 +89,7 @@ versions are released. ### Samples The stack also includes a large collection of -[platform-independent sample applications](docs/samples.md) that turn its +[platform-independent sample applications](docs/Samples.md) that turn its core services and companion models into runnable client/server workflows. More applications, including platform-specific examples, are available in the companion diff --git a/docs/AI.md b/docs/AI.md index de377b8397..28811395b6 100644 --- a/docs/AI.md +++ b/docs/AI.md @@ -1,18 +1,31 @@ # AI Model Management developer guide -This guide documents the `Opc.Ua.AI`, `Opc.Ua.AI.Server`, -`Opc.Ua.AI.Client` and `Opc.Ua.AI.Inference` package family — the .NET -implementation of the draft *OPC UA — AI Model Management and Inference* -companion specification. +The `Opc.Ua.AI`, `Opc.Ua.AI.Server`, `Opc.Ua.AI.Client`, and +`Opc.Ua.AI.Inference` packages implement the draft +*OPC UA — AI Model Management and Inference* companion specification in .NET. > **Draft.** The namespace `http://opcfoundation.org/UA/AI/` and every NodeId > in it are provisional until the working group publishes the specification. -AI Model Management publishes model sources, models, datasets, deployments and -inference methods through OPC UA. The control plane is OPC UA: clients discover -what is available, read the provenance and trust-boundary metadata, call -`Invoke` or `InvokeAsync`, and transfer large artefacts through the standard -file-transfer types. +AI Model Management exposes model sources, models, datasets, deployments, and +inference methods through OPC UA. Clients can discover available resources and +read provenance and trust-boundary metadata. They can also invoke `Invoke` or +`InvokeAsync` and transfer large artefacts with standard file-transfer types. + +## Contents + +- [Packages](#packages) +- [Model](#model) +- [Minimal hosted server](#minimal-hosted-server) +- [Hosting API](#hosting-api) + - [`AIOptions`](#aioptions) + - [`InferenceBackendOptions`](#inferencebackendoptions) +- [Client surface](#client-surface) +- [Inference backends](#inference-backends) +- [Invocation inputs and parameters](#invocation-inputs-and-parameters) +- [Example](#example) +- [Limitations](#limitations) +- [See also](#see-also) ## Packages @@ -29,19 +42,25 @@ OpenAI or other vendor SDK dependency. ## Model -The Server publishes one AI root below the Server object. Under it are model -sources, deployments, models, datasets and jobs. A deployment describes where -inference runs, whether egress is permitted, whether input may be retained, the -maximum inline payload size, and the model source it uses. +The Server publishes one AI root below the Server object. The root contains: + +- Model sources +- Deployments +- Models +- Datasets +- Jobs + +Each deployment describes where inference runs and identifies its model +source. It also records egress permission, input-retention status, and the +maximum inline payload size. Two properties are especially important: -- `ModelUsed` is returned with an inference result so a fallback cannot answer - silently. A caller can distinguish "the primary model answered" from "a - degraded fallback answered". -- `CredentialReference` is a name only. The credential value is resolved inside - the Server process by an `ICredentialResolver` and is never placed in the - address space. +- An inference result includes `ModelUsed`, so callers can tell whether the + primary model or a degraded fallback answered. +- `CredentialReference` is a name only. An `ICredentialResolver` resolves the + credential value inside the Server process. The address space contains only + the reference name, never the credential value. Large payloads use the standard Part 5 `FileType` transfer flow. Asynchronous inference jobs use the Part 10 program lifecycle so clients can monitor state @@ -89,8 +108,8 @@ await app.RunAsync().ConfigureAwait(false); `AddRestChatCompletionsAIChatClientFactory()` is the sample-friendly factory: it creates an `IChatClient` over the configured OpenAI-compatible endpoint without adding a vendor SDK. A production host can instead register its own -`IChatClientFactory` that creates `IChatClient` instances from Azure, OpenAI, -Ollama or an on-device runtime package. +`IChatClientFactory`. That factory can create `IChatClient` instances for +Azure, OpenAI, Ollama, or an on-device runtime. The direct construction path remains available for hosts that do not use the generic hosting stack: @@ -203,35 +222,46 @@ when you already own an `ISession`. and probe reachability. Two implementations ship: - `ChatClientInferenceBackend` wraps `Microsoft.Extensions.AI.IChatClient`. - This is the default because it lets the host choose any SDK or local runtime - that implements the abstraction without changing the OPC UA address space. + This is the default. Hosts can pair it with any SDK or local runtime that + implements the abstraction. The OPC UA address space stays unchanged. - `RestChatCompletionsBackend` speaks the OpenAI-compatible REST chat-completions contract directly. Use it when that REST shape is the actual wire contract and no `IChatClient` is available. -Both hosted and on-device deployments use the same OPC UA nodes. The difference -is configuration: endpoint, credentials, data jurisdiction and egress. +Both hosted and on-device deployments use the same OPC UA nodes. Their +configuration identifies the endpoint, credentials, data jurisdiction, and +egress. ## Invocation inputs and parameters `Invoke` and `InvokeAsync` accept an inline `Payload` or a `PayloadUri`, but the -current server implementation supports inline payloads only. It returns -`BadNotSupported` for URI-only input rather than accepting it and running a -backend with no request body. +current server accepts inline payloads only. It returns `BadNotSupported` for +URI-only input rather than accepting it and running a backend with no request +body. -Both built-in backends support `temperature` (0 through 2), `max_tokens` -(positive integer), and `top_p` (0 through 1). Parameter values may be supplied -as strings, as any built-in integer or floating-point type, or as booleans, and -are converted using the invariant culture. They are forwarded through -synchronous, asynchronous, fallback, and oversized-transfer execution, and -rejected when malformed or unsupported. For the REST chat-completions backend, -an explicit OPC UA parameter overrides the corresponding field in the JSON -request body; body fields without a matching parameter are left unchanged. +Both built-in backends accept these parameters: -The server publishes `SpecificationVersion` from the source-generated -`Opc.Ua.AI.ModelVersions.Target` constant, which is derived from the AI -NodeSet's target model version. Updating the NodeSet therefore updates the -published version without a second hand-maintained literal. +- `temperature`: 0 through 2 +- `max_tokens`: positive integer +- `top_p`: 0 through 1 + +Clients can provide values as strings, built-in integer or floating-point +values, or booleans. Both backends convert values using the invariant culture +and forward them through these execution paths: + +- Synchronous and asynchronous calls +- Fallback execution +- Oversized-transfer execution + +Both backends reject malformed or unsupported parameters. In the REST +chat-completions backend, an explicit OPC UA parameter overrides the matching +field in the JSON request body. The backend preserves body fields without a +matching parameter. + +The server sets `SpecificationVersion` from the source-generated +`Opc.Ua.AI.ModelVersions.Target` constant. The AI NodeSet's target model +version determines this constant, so updating the NodeSet also updates the +published version without a separately maintained literal. ## Example @@ -248,8 +278,8 @@ dotnet run --project samples/AI/ModelManagementServer dotnet run --project samples/AI/ModelManagementClient ``` -Set `InferenceBackend__Kind=RestChatCompletions` when testing an endpoint that -must be reached through the REST backend directly. Configure +Set `InferenceBackend__Kind=RestChatCompletions` to select the REST backend for +an endpoint that requires direct REST access. Configure `FallbackInferenceBackend__Kind` independently when the fallback uses a different wire contract from the primary. @@ -260,13 +290,13 @@ different wire contract from the primary. - The sample publishes a real `LearningJobType` instance and a real `SamplesCollected` counter. Host-level code can call the server-side accounting API when ground-truth corrections arrive, including empty or - retracted observations. Retraining, candidate generation and promotion are - deliberately not simulated. + retracted observations. The sample does not simulate retraining, candidate + generation, or promotion. - `IChatClient` has no standard model-enumeration method, so hosts that need a catalogue should configure `InferenceBackendOptions.Models`. -- The libraries do not reference vendor SDKs. If a provider package is needed, - add it in the hosting application and expose it through `IChatClientFactory`. -- Native AOT is disabled for the AI inference/sample projects because the +- The libraries do not reference vendor SDKs. If your hosting application needs + a provider package, add it there and expose it through `IChatClientFactory`. +- The AI inference and sample projects disable Native AOT because the `Microsoft.Extensions.AI` ecosystem uses reflection in areas this repository builds with warnings as errors. diff --git a/docs/Aggregates.md b/docs/Aggregates.md index 1167476b71..4901e78c28 100644 --- a/docs/Aggregates.md +++ b/docs/Aggregates.md @@ -2,14 +2,30 @@ ## Overview -The .NET Standard stack implements **OPC UA Part 13 (OPC 10000-13) v1.05.07 Aggregates** on the server -side, computed over historical data retrieved through [Historical Access](HistoricalAccess.md) (Part 11). -All **37 standard aggregate functions** are supported and advertised through the address space. +The .NET Standard stack implements **OPC UA Part 13 (OPC 10000-13) v1.05.07 Aggregates** on the +server. The server computes aggregates from historical data retrieved through [Historical Access](HistoricalAccess.md) +(Part 11) and advertises support for all **37 standard aggregate functions** through the address space. -Aggregates are requested with the `HistoryRead` service using `ReadProcessedDetails` (a `startTime`, -`endTime`, `ProcessingInterval`, one or more `AggregateType` NodeIds, and an optional -`AggregateConfiguration`). The server divides the time range into intervals of `ProcessingInterval` -milliseconds and produces one aggregate value per interval. +Clients request aggregates with the `HistoryRead` service and `ReadProcessedDetails`. A request includes: + +- `startTime` and `endTime` +- `ProcessingInterval` in milliseconds +- One or more `AggregateType` NodeIds +- An optional `AggregateConfiguration` + +The server divides the time range into intervals of `ProcessingInterval` milliseconds and produces one +aggregate value per interval. + +## Contents + +- [Overview](#overview) +- [Supported aggregate functions](#supported-aggregate-functions) +- [Architecture](#architecture) + - [`AnnotationCount`](#annotationcount) +- [Server configuration](#server-configuration) +- [Client usage](#client-usage) +- [Notes and limitations](#notes-and-limitations) +- [References](#references) ## Supported aggregate functions @@ -23,8 +39,8 @@ milliseconds and produces one aggregate value per interval. | Quality / time-in-state | `DurationGood`, `DurationBad`, `PercentGood`, `PercentBad`, `WorstQuality`, `WorstQuality2` | | Statistics | `StandardDeviationSample`, `VarianceSample`, `StandardDeviationPopulation`, `VariancePopulation` | -The exact per-aggregate semantics (type, bounding behaviour, timestamp, status-code rules, special cases) -follow Part 13 §5.4.3.4–§5.4.3.40. +Part 13 §§5.4.3.4–5.4.3.40 defines each aggregate's result type, bounding behaviour, timestamp, +status-code rules, and special cases. ## Architecture @@ -40,23 +56,23 @@ AsyncCustomNodeManager / CustomNodeManager2 → HistorianDispatcher.DispatchPro ``` - **`AggregateManager`** (`Opc.Ua.Server`) owns the registered aggregate factories, the server default - `AggregateConfiguration`, and the `MinimumProcessingInterval`. The standard functions are registered via - `Aggregators` and advertised into `Server.ServerCapabilities.AggregateFunctions` and + `AggregateConfiguration`, and the `MinimumProcessingInterval`. `Aggregators` registers the standard + functions. The server advertises them in `Server.ServerCapabilities.AggregateFunctions` and `HistoryServerCapabilities.AggregateFunctions`. - **`IAggregateCalculator`** implementations (`AggregateCalculator` and the specialized `Average`/`MinMax`/`Count`/`StartEnd`/`Status`/`StdDev` calculators) compute the aggregates from a stream of raw `DataValue`s. - A historian provider may compute aggregates itself by implementing `IHistorianProcessedProvider` (native push-down). When it does not, the framework streams raw values through the calculator, - using the node's `Stepped` capability. Completed intervals are drained after each raw sample; - the fallback stops with `Bad_TooManyOperations` if its 100,000-output buffer limit is exceeded. + using the node's `Stepped` capability. After each raw sample, the framework collects results for completed intervals. + The fallback returns `Bad_TooManyOperations` if it exceeds the 100,000-output buffer limit. ### AnnotationCount `AnnotationCount` (Part 13 §5.4.3.20) counts **Annotations** in each interval, not raw data values. -It is therefore computed from the node's annotation history via `IHistorianAnnotationProvider`, not from -the raw-value calculator. If the resolved provider does not expose annotation history, an `AnnotationCount` -request returns `Bad_AggregateNotSupported`. +It uses the node's annotation history through `IHistorianAnnotationProvider`, not the raw-value calculator. +If the resolved provider does not expose annotation history, an `AnnotationCount` request returns +`Bad_AggregateNotSupported`. ## Server configuration @@ -64,7 +80,7 @@ request returns `Bad_AggregateNotSupported`. | Property | Default | Meaning | |---|---|---| -| `TreatUncertainAsBad` | **`true`** | Whether Uncertain samples are treated as Bad when computing the aggregate `StatusCode` (Part 13 §4.2.1.2). | +| `TreatUncertainAsBad` | **`true`** | Treat Uncertain samples as Bad when computing the aggregate `StatusCode` (Part 13 §4.2.1.2). | | `PercentDataBad` | `100` | Minimum % of Bad data in an interval for the interval `StatusCode` to be Bad. | | `PercentDataGood` | `100` | Minimum % of Good data in an interval for the interval `StatusCode` to be Good. | | `UseSlopedExtrapolation` | `false` | Stepped (hold-last) vs sloped extrapolation past the last value. Ignored for Simple Bounds. | @@ -130,8 +146,8 @@ await foreach (DataValue value in session.Historian().ReadProcessedAsync( } ``` -Discover the aggregates a server supports with `session.Historian().GetServerCapabilitiesAsync(...)` or by -browsing `Server.ServerCapabilities.AggregateFunctions`. +Call `session.Historian().GetServerCapabilitiesAsync(...)` to discover supported aggregates, or browse +`Server.ServerCapabilities.AggregateFunctions`. ## Notes and limitations diff --git a/docs/AlarmsAndConditions.md b/docs/AlarmsAndConditions.md index 046c794df1..c70fd2ceba 100644 --- a/docs/AlarmsAndConditions.md +++ b/docs/AlarmsAndConditions.md @@ -1,9 +1,9 @@ # Alarms and Conditions (OPC UA Part 9) -This guide describes the OPC UA Part 9 *Alarms & Conditions* support in -this stack: how to build alarm-aware servers, how to consume alarm events -on the client, and how the new helpers (latched alarms, alarm groups, -suppression engine, alarm metrics, streaming alarm records) fit together. +This guide covers the stack's OPC UA Part 9 *Alarms & Conditions* support. +It explains how to build alarm-aware servers and consume alarm events on +clients. It also introduces the helpers for latched alarms, alarm groups, +suppression, alarm metrics, and streaming alarm records. For the formal model, see [OPC UA Part 9 — Alarms and Conditions](https://reference.opcfoundation.org/specs/OPC-10000-9/full). @@ -16,6 +16,7 @@ For the formal model, see - [Re-alarming](#re-alarming) - [Audible alarms and silencing](#audible-alarms-and-silencing) - [Suppression, out-of-service, shelving](#suppression-out-of-service-shelving) + - [Filtered retain](#filtered-retain) - [Alarm groups and first-in-group](#alarm-groups-and-first-in-group) - [Alarm metrics (rate tracking)](#alarm-metrics-rate-tracking) - [Vetoing alarm operations](#vetoing-alarm-operations) @@ -49,19 +50,18 @@ For the formal model, see | Decode raw event fields | n/a | `EventRecordDecoderRegistry.Default.Decode` | | Build an event filter | n/a | `{Type}Record.EventFilters.Build(registry?)` | -`AlarmClient` is obtained from any `ISession` and a telemetry context; -internally it delegates every Part 9 method call to the matching -source-generated `*TypeClient` proxy (so there is exactly one place -that knows each method NodeId — the generator): +Create `AlarmClient` from any `ISession` and a telemetry context. It delegates +each Part 9 method call to the matching source-generated `*TypeClient` proxy. +The generated proxy is the single source for each method NodeId: ```csharp AlarmClient alarms = session.GetAlarmClient(telemetry); ``` -For dependency-injected hosts use the +Dependency-injected hosts can call the [`builder.AddAlarms()`](DependencyInjection.md#alarms-and-conditions) -extension and resolve `AlarmClientFactory` (which threads the -host's telemetry into the client for you): +extension and resolve `AlarmClientFactory`. The factory passes the host's +telemetry context to the client: ```csharp var factory = sp.GetRequiredService(); @@ -111,11 +111,10 @@ alarm.SetActiveState(context, active: true); alarm.ReportEvent(context, alarm); ``` -The optional state nodes are created the way you would expect on a -generated `AlarmConditionState`: assign to `alarm.SilenceState`, -`alarm.OutOfServiceState`, `alarm.LatchedState`, etc. before calling -`Create`. The quickstart reference server creates these via -`AlarmConditionTypeHolder.Initialize` — +Before calling `Create`, assign any optional state nodes to properties such as +`alarm.SilenceState`, `alarm.OutOfServiceState`, and `alarm.LatchedState`. +The quickstart reference server creates these nodes through +`AlarmConditionTypeHolder.Initialize` in `samples/Quickstarts.Servers/Alarms/AlarmHolders/`. ### Driving state from your process @@ -147,12 +146,12 @@ alarm* (Part 9 §4.8). The semantics: - `SetActiveState(context, false)` — `ActiveState` reflects the real process state, but `LatchedState` stays `true`. - `Reset` (server-side method, auto-wired) — clears `LatchedState`. - The `Reset` method validates **all** preconditions before accepting: - enabled, not active, acknowledged, confirmed (if `ConfirmedState` is - present). Any other state returns `Bad_InvalidState`. + `Reset` accepts the request only when the alarm is enabled, inactive, and + acknowledged, and when it is confirmed if `ConfirmedState` is present. + Otherwise, it returns `Bad_InvalidState`. -Latched alarms are *retained* (`Retain = true`) for as long as -`LatchedState.Id` is `true`, so a client refresh sees them. +The server retains latched alarms (`Retain = true`) while `LatchedState.Id` +is `true`, so clients can see them during a refresh. ### Re-alarming @@ -173,18 +172,18 @@ if (alarm.IsReAlarmEnabled && alarm.ActiveState.Id.Value alarm.ResetReAlarmRepeatCount(context); ``` -`ProcessReAlarm`: +`ProcessReAlarm` performs these actions: - clears `AckedState` (forces re-acknowledgement) - clears `SilenceState` (audible annunciation resumes) - increments `ReAlarmRepeatCount` -- calls `ReportStateChange` so the new event is published +- calls `ReportStateChange` to publish the new event ### Audible alarms and silencing -If `AudibleEnabled = true` and `AudibleSound` is populated, the -`UpdateAudibleState` helper takes care of clearing the silence state -when the alarm activates (so the next activation is audible again): +When `AudibleEnabled = true` and `AudibleSound` has a value, +`UpdateAudibleState` clears the silence state on activation. The next +activation is then audible: ```csharp ByteString sound = LoadWavFile(); @@ -197,8 +196,8 @@ server) sets `SilenceState.Id = true`. ### Suppression, out-of-service, shelving All three states contribute to the `SuppressedOrShelved` boolean. The -state-type setters keep that flag in sync — clearing one does **not** -clear `SuppressedOrShelved` while the others are still active: +state-type setters keep the flag in sync. Clearing one state does **not** +clear `SuppressedOrShelved` while another state remains active: ```csharp alarm.SetSuppressedState(context, true); // SuppressedOrShelved = true @@ -209,12 +208,10 @@ alarm.SetOutOfServiceState(context, false); // SuppressedOrShelved = false ### Filtered retain -Part 9 B.1.4 lets a server give each client a Retain flag that takes -that client's event filter into account. With filtered retain on, a -condition that drops out of the scope of a client's where clause -reports one final event — so that client sees `Retain = false` even -though nothing changed on the server, and the alarm stays active for -everyone else. +Part 9 B.1.4 lets a server set a client-specific `Retain` flag based on +that client's event filter. When a condition no longer matches a client's +where clause, the server sends that client a final event with +`Retain = false`. The alarm remains active for everyone else. Opt a condition in by setting `SupportsFilteredRetain`: @@ -236,18 +233,17 @@ Notes on the shape of that property: since nothing that walks the instance children would. * Each condition and branch is tracked separately, so one branch leaving filter scope does not affect its siblings or its parent. -* `ConditionRefresh` participates: refreshed conditions are evaluated - against the where clause and re-prime the tracking. +* `ConditionRefresh` reevaluates refreshed conditions against the where + clause and primes the tracking again. * Changing a monitored item's where clause with `ModifyMonitoredItems` - discards the tracking, because it describes the *previous* filter. - Durable subscriptions carry it across a restart. -* The trailing event carries a client specific `Retain = false` - (Part 9, 5.5.2, Figure 11), whatever the server's own `Retain` says — a - trailing event with `Retain = true` would tell the client to keep an - alarm it should drop. The override is applied while that one client's - event fields are read; the shared event snapshot is untouched, so a - client whose filter the condition still passes keeps receiving the - server's real value. + discards tracking for the previous filter. Durable subscriptions preserve + tracking across a restart. +* Each client receives a trailing event with `Retain = false` (Part 9, + 5.5.2, Figure 11), regardless of the server's `Retain` value. Otherwise, + the client would keep an alarm that it should drop. The override applies + only to that client's event fields; it leaves the shared event snapshot + unchanged. Other clients whose filters still include the condition keep + receiving the server's actual value. ### Alarm groups and first-in-group @@ -291,21 +287,19 @@ engine.OnFirstInGroupActiveChanged(context, masterTripAlarm, tripGroup, firstActive: true); ``` -The first `Evaluate` call always applies the current state, so -clients see a coherent suppression state immediately after -registration — there is no edge required. +The first `Evaluate` call applies the current state. Clients therefore see +a coherent suppression state immediately after registration; no state +transition is required. -> **Live demo:** The `samples/Quickstarts.Servers/Alarms/ -> AlarmNodeManager.cs` reference implementation wires this up -> end-to-end. It exposes a `/Alarms/AnalogGroup` (`AlarmGroupType`) -> containing every analog-source alarm and a writable -> `/Alarms/MaintenanceMode` boolean. Writing `true` to -> MaintenanceMode runs `AlarmSuppressionEngine.Evaluate(...)` and -> suppresses every group member; writing `false` clears -> suppression on the next evaluation. The reference server's node -> manager itself derives from `AsyncCustomNodeManager`, so the same -> file is a worked example of porting a Part 9 demo to the modern -> async base class. +> **Live demo:** The reference implementation in +> `samples/Quickstarts.Servers/Alarms/AlarmNodeManager.cs` exposes an +> `/Alarms/AnalogGroup` (`AlarmGroupType`) containing every analog-source +> alarm and a writable `/Alarms/MaintenanceMode` boolean. Writing `true` to +> `MaintenanceMode` runs `AlarmSuppressionEngine.Evaluate(...)` and +> suppresses every group member. Writing `false` clears suppression on the +> next evaluation. The reference node manager derives from +> `AsyncCustomNodeManager`, so this file also demonstrates the modern async +> base class. ### Alarm metrics (rate tracking) @@ -375,15 +369,13 @@ audit event — it happens inside the method handler when ### `AlarmClient` — typed operations `AlarmClient` is the strongly-typed client API for Part 9 methods. -Each method delegates to the matching source-generated -`*TypeClient` proxy (`ConditionTypeClient`, -`AcknowledgeableConditionTypeClient`, -`AlarmConditionTypeClient`, `DialogConditionTypeClient`, -`ShelvedStateMachineTypeClient`), passing the caller-supplied -`conditionId` as the proxy's `ObjectId`. This honours the Part 9 -§5.5.4 idiom (`ConditionId` is acceptable as `ObjectId`) and keeps -exactly one source of truth — the generated proxy — for every -method NodeId and argument shape. +Each method delegates to the matching source-generated proxy: +`ConditionTypeClient`, `AcknowledgeableConditionTypeClient`, +`AlarmConditionTypeClient`, `DialogConditionTypeClient`, or +`ShelvedStateMachineTypeClient`. The proxy receives the caller-supplied +`conditionId` as its `ObjectId`, following the Part 9 §5.5.4 idiom that +accepts `ConditionId` as `ObjectId`. The generated proxy remains the single +source for each method NodeId and argument shape. ```csharp AlarmClient alarms = session.GetAlarmClient(telemetry); @@ -417,10 +409,9 @@ await alarms.UnshelveAsync(conditionId); ArrayOf groups = await alarms.GetGroupMembershipsAsync(conditionId); ``` -The `*Async(... comment ...)` overloads automatically pick the -spec-defined `*2` method when a non-empty comment is supplied -(`Suppress2`, `Unsuppress2`, `RemoveFromService2`, `PlaceInService2`, -`Reset2`). +The `*Async(... comment ...)` overloads call the spec-defined `*2` method +when the comment is non-empty: `Suppress2`, `Unsuppress2`, +`RemoveFromService2`, `PlaceInService2`, and `Reset2`. ### Subscribing to alarms with `IAsyncEnumerable` @@ -473,11 +464,11 @@ await streaming.SubscribeAlarmsAsync(ObjectIds.Server) Alarm and condition records are **source-generated** by the `Opc.Ua.SourceGeneration` analyzer (see the `EventRecordGenerator`). -For every `ObjectType` whose base-type chain ends at `BaseEventType`, -the generator emits a `partial record {Type}Record` deriving from the -record of its parent type, exposing one init-only property per -declared field. The standard NodeSet produces the following hierarchy -(abridged — only the most commonly observed types are shown): +For every `ObjectType` derived from `BaseEventType`, the generator emits +a `partial record {Type}Record`. Each generated record derives from its +parent record and exposes one init-only property for each declared field. +The standard NodeSet produces the following hierarchy, abridged to show +the most commonly observed types: ``` EventRecord (anchor; hand-written) @@ -495,12 +486,11 @@ EventRecord (anchor; hand-written) └── DiscrepancyAlarmTypeRecord ``` -Vendor models that derive from any of these types **automatically** -get their own `*TypeRecord` deriving from the closest standard -ancestor — no checked-in code, no manual class definitions. Add a -hand-written `partial record VibrationAlarmTypeRecord` in your project -to extend the generated declaration with computed properties or -custom helpers. +Vendor models that derive from these types automatically get their own +`*TypeRecord`, which derives from the closest standard ancestor. No +checked-in code or manual class definition is needed. Add a hand-written +`partial record VibrationAlarmTypeRecord` to your project to extend the +generated declaration with computed properties or custom helpers. The decoder upgrades the record type based on which fields are populated in the event. A simple `switch` on the record type gives you @@ -516,26 +506,24 @@ if (record is CertificateExpirationAlarmTypeRecord cert) ``` The shared `ConditionTypeRecord.ConditionId` property is a hand-written -alias for `SourceNode` — Part 9 of the OPC UA specification defines -the "ConditionId" as the NodeId of the condition object that fired -the event, which is reported through the `SourceNode` event field. +alias for `SourceNode`. Part 9 defines `ConditionId` as the NodeId of the +condition object that raised the event. The event reports this NodeId in +the `SourceNode` field. ### Source-generated decoders + `EventRecordDecoderRegistry` -Every record emitted by the `EventRecordGenerator` also exposes a -nested `static class Decoder` with a positional `StandardFields` -table and a `Decode(IReadOnlyList)` method that populates -own + inherited init-only properties. A per-file -`Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry)` -extension registers every emitted decoder with a caller-supplied -registry. +Every record emitted by `EventRecordGenerator` also exposes a nested +`static class Decoder`. The decoder provides a positional `StandardFields` +table and a `Decode(IReadOnlyList)` method that populates the +record's own and inherited init-only properties. A per-file +`Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry)` extension +registers each generated decoder with a caller-supplied registry. -The process-wide `EventRecordDecoderRegistry.Default` ships with the -standard UA model pre-registered. It routes by the event's -`EventType` field and walks the OPC UA event-type hierarchy through -an optional `SuperTypeResolver` when the exact type is not -registered. Vendor models register their generated extensions via -`Register{Prefix}Decoders` or +The process-wide `EventRecordDecoderRegistry.Default` registers the +standard UA model. It routes by the event's `EventType` field. If the +exact type is not registered, it walks the OPC UA event-type hierarchy +through an optional `SuperTypeResolver`. Vendor models can register their +generated extensions with `Register{Prefix}Decoders` or `CreateChildScope().Register{Prefix}Decoders()` for test isolation. ```csharp @@ -553,14 +541,13 @@ EventRecord? vendorRec = app.Decode(eventFields); Every generated `{Type}Record` exposes a nested `static class EventFilters` alongside its `Decoder` block. The -`Build(registry?)` factory produces an `EventFilter` whose where -clause restricts events to `OfType({recordTypeId})` and whose -select clauses come from the supplied registry's composed -`StandardFields` (defaults to -`EventRecordDecoderRegistry.Default`). The returned filter pairs -cleanly with `EventRecordDecoderRegistry.Decode` — the registry -remaps the composed positions to each decoder's own layout -before invoking it, so vendor models extend transparently. +`Build(registry?)` factory returns an `EventFilter`. Its where clause +restricts events to `OfType({recordTypeId})`, and its select clauses come +from the supplied registry's composed `StandardFields` (by default, +`EventRecordDecoderRegistry.Default`). Use the filter with +`EventRecordDecoderRegistry.Decode`. The registry remaps composed field +positions to each decoder's layout before decoding, so vendor models +extend transparently. ```csharp // Filter for alarm events: @@ -583,9 +570,9 @@ var app = EventRecordDecoderRegistry.Default EventFilter vendorFilter = VibrationAlarmTypeRecord.EventFilters.Build(app); ``` -The same registry should be passed to `Subscribe*Async` -(through the `registry:` parameter) so the streaming side decodes -through the registry that built the filter. +Pass the same registry to `Subscribe*Async` through its `registry:` +parameter. This ensures the streaming side uses the registry that built +the filter. ### Dialog conditions @@ -609,12 +596,11 @@ await foreach (DialogConditionTypeRecord dialog in streaming.SubscribeDialogsAsy } ``` -The `OkResponse` / `CancelResponse` / `DefaultResponse` properties on -the *server-side* `DialogConditionType` (Part 9 §5.6.2) carry their -canonical indices for clients to read separately via Read service if -the application needs them; they are not surfaced in the standard -`DialogConditionTypeRecord` (which only carries the dialog prompt + -active state). +The server-side `DialogConditionType` properties `OkResponse`, +`CancelResponse`, and `DefaultResponse` carry canonical indices (Part 9 +§5.6.2). Applications can read these indices separately through the Read +service. The standard `DialogConditionTypeRecord` does not include them; +it contains only the dialog prompt and active state. ### `ConditionRefresh` @@ -628,9 +614,9 @@ await alarms.ConditionRefreshAsync(subscriptionId); await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId); ``` -The classic `ISubscription.ConditionRefreshAsync` on the streaming -subscription is also available — they are equivalent when the -`AlarmClient` and the `IStreamingSubscription` are bound to the same +The streaming subscription also provides +`ISubscription.ConditionRefreshAsync`. Both refresh methods are +equivalent when `AlarmClient` and `IStreamingSubscription` use the same session. ## Reference diff --git a/docs/AliasNames.md b/docs/AliasNames.md index 2f5970a488..370d27db12 100644 --- a/docs/AliasNames.md +++ b/docs/AliasNames.md @@ -1,12 +1,12 @@ # OPC UA Part 17 — Alias Names -OPC UA Part 17 defines a small but valuable address-space pattern: a -hierarchy of human-readable **alias names** that point at one or more -nodes via a non-hierarchical reference type. Clients can search the -hierarchy by wildcard pattern and resolve a name to its targets without -needing to know the target NodeId in advance — useful for tag-naming -schemes (PI / SCADA / DCS), pub/sub topic registries, MES integration, -and any scenario where humans pick names but machines need ids. +OPC UA Part 17 defines a hierarchy of human-readable **alias names**. +Each alias uses a non-hierarchical reference to point to one or more +nodes. Clients can search the hierarchy with wildcard patterns and +resolve aliases without knowing target NodeIds in advance. This pattern +suits tag-naming schemes (PI / SCADA / DCS), pub/sub topic registries, +MES integration, and other scenarios where people choose names for +machine-addressed nodes. This stack ships full Part 17 support in **`Opc.Ua.Server`** (server side) and **`Opc.Ua.Client`** (client side). The implementation covers: @@ -28,12 +28,27 @@ side) and **`Opc.Ua.Client`** (client side). The implementation covers: | §9.4 | Well-known `Topics (i=23488)` | ✔ wired; optional methods opt-in — see below | | Annex D | PubSub replication (LastChange notifications) | ✔ transport-agnostic — see below | +## Contents + +- [Server side](#server-side--opcuaserveraliasnames) + - [Quick start — serving standard categories](#quick-start--serving-standard-categories) + - [Quick start — application-defined categories](#quick-start--application-defined-categories) + - [Browsable alias nodes](#browsable-alias-nodes) + - [Custom backend](#custom-backend) +- [Client side](#client-side--opcuaclientaliasnames) + - [`AliasNameResolver`](#aliasnameresolver--cached-aliasnodeid) +- [Spec deviations / wrinkles](#spec-deviations--wrinkles) +- [Annex D — PubSub LastChange notifications](#annex-d--pubsub-lastchange-notifications) + - [Server-side PubSub](#server-side--opcuaserveraliasnamespubsub) + - [Client-side PubSub](#client-side--opcuaclientaliasnamespubsub) +- [See also](#see-also) + ## Server side — `Opc.Ua.Server.AliasNames` Search patterns use the shared `LikePattern` parser and matcher, with case-sensitive whole-string -matching. Backslashes escape literal characters inside and outside -character sets; `[^...]` negates a set (`[!...]` remains accepted for -compatibility). Wildcards match line breaks. Trailing escapes, malformed sets, +matching. Backslashes escape literal characters both inside and outside +character sets. `[^...]` negates a set; `[!...]` remains accepted for +compatibility. Wildcards match line breaks. Trailing escapes, malformed sets, descending ranges, and unescaped `^` outside the start of a set return `BadInvalidArgument` from both FindAlias variants even with an empty store. All alias matches in one search share a 100 ms deadline; exceeding it @@ -180,23 +195,25 @@ independent of the custom `AliasNameNodeManagerOptions` default of `AddServer()` rejects these DI-only settings rather than silently ignoring them. -`DiagnosticsNodeManager.MaterializeRegisteredAliasNameNodesAsync` does -that for every registered store: one `AliasNameType` instance per alias -— BrowseName carrying the alias name, `AliasFor` references to its -targets, and the inverse `HasAlias` reference added on each local -target — plus an `AliasNameCategoryType` instance for every store -category the standard NodeSet does not already ship (nested categories -included; a root category from the store is organized under the -standard `Aliases` object so it stays browse-discoverable). Remote -targets get their server URI registered in the server's `ServerUris` -table and referenced with the matching `ServerIndex`. Server-defined -BrowseNames that a descriptor left in namespace 0 — reserved for -OPC-Foundation-defined names — are re-homed into the diagnostics -namespace; Part 17 clients compare alias names ignoring the namespace, -so this is transparent to them. The pass is idempotent, and only -creates category nodes whose NodeId lies in the diagnostics namespace — -a descriptor pointing anywhere else is skipped with a warning rather -than claiming another manager's (or the standard NodeSet's) ids. +`DiagnosticsNodeManager.MaterializeRegisteredAliasNameNodesAsync` +materializes every registered store. For each alias, it creates an +`AliasNameType` instance whose BrowseName is the alias name. It adds +`AliasFor` references to the alias targets and inverse `HasAlias` +references to local targets. + +The method also creates an `AliasNameCategoryType` instance for every store +category absent from the standard NodeSet, including nested categories. It +organizes a root category under the standard `Aliases` object so clients can +browse to it. For each remote target, the manager registers its server URI in +the server's `ServerUris` table and uses the matching `ServerIndex`. + +The manager moves server-defined BrowseNames in namespace 0, reserved for +OPC Foundation names, to the diagnostics namespace. Part 17 clients compare +alias names without considering the namespace, so this change is transparent. +The pass is idempotent and creates category nodes only when their NodeId lies +in the diagnostics namespace. If a descriptor points elsewhere, the manager +skips that category with a warning rather than claiming IDs owned by another +manager or the standard NodeSet. Custom diagnostics-node-manager implementations can still call the helper from an overridden `CreateAddressSpaceAsync`, after `base` has loaded the standard categories: @@ -214,11 +231,11 @@ public override async ValueTask CreateAddressSpaceAsync( } ``` -The same pass also instantiates the **optional Part 17 members** — -`FindAliasVerbose`, `AddAliasesToCategory`, `DeleteAliasesFromCategory` -and `LastChange` — on every category whose descriptor declares them -through `AliasNameCapabilities`, including the well-known ones the -standard NodeSet ships without them: +The same pass also creates the **optional Part 17 members** +`FindAliasVerbose`, `AddAliasesToCategory`, `DeleteAliasesFromCategory`, +and `LastChange` for each category whose descriptor declares them through +`AliasNameCapabilities`. This includes well-known categories for which the +standard NodeSet does not declare these members: ```csharp var tagVariables = new AliasNameCategoryDescriptor( @@ -249,46 +266,49 @@ builder.ConfigureAliasNames(options => }); ``` -Each live-materialization host owns one background worker and at most one pending -refresh signal. Category and ancestor notifications coalesce into a pass -over store roots. A completed query is applied only while its generation -is current; query failures leave existing nodes and references intact and -are logged. Reconciliation retains unchanged node instances, updates only -changed associations, and applies inverse `HasAlias` references to their -actual local targets. `LastChange` reads the current store version rather -than a potentially delayed event value, including across counter rollover. -Asynchronous host disposal cancels and drains the worker, with a fixed -five-second drain limit and a warning if a provider does not stop. +Each live-materialization host owns one background worker and at most one +pending refresh signal. Category and ancestor notifications coalesce into a +single pass over store roots. -The browse view carries `AliasFor` associations only: an entry a store -holds under an unrelated reference type is not a Part 17 §6.2 alias -association, so it is served by `FindAlias` (with a matching filter) -but not materialized. Entries stored under an `AliasFor` *subtype* are -materialized, but always as the base `AliasFor` reference — the -verbose record does not carry the concrete reference type. +The worker applies a completed query only if its generation is current. If a +query fails, it leaves existing nodes and references intact and logs the +failure. The worker retains unchanged node instances, updates only +changed associations, and applies inverse `HasAlias` references to their +actual local targets. It reads `LastChange` from the current store version, +not a possibly delayed event value, including across counter rollover. + +During asynchronous host disposal, the host cancels and drains the worker. +Disposal waits up to five seconds and logs a warning if a provider does not +stop. + +The browse view materializes only `AliasFor` associations. If a store holds an +entry under an unrelated reference type, the entry is not a Part 17 §6.2 alias +association. `FindAlias` can return it when the filter matches, but the +materializer does not create an alias node for it. The materializer also +creates entries stored under an `AliasFor` subtype, but uses the base +`AliasFor` reference because the verbose record does not carry the concrete +reference type. Two things the caller controls: -* Give category descriptors a BrowseName in a namespace the server - owns. The store stamps that namespace onto every alias - `QualifiedName` it reports, and the materializer re-homes ns=0 - BrowseNames out of the reserved OPC Foundation namespace — with a - server-owned descriptor namespace the query results and the - browsable nodes agree, so a returned alias name resolves via - `TranslateBrowsePathsToNodeIds`. The reference server uses the - diagnostics namespace for all its descriptors. -* Part 17 §9 constrains `Topics` aliases to `PublishedDataSetType` - targets. Neither the store nor `AddAliasesToCategory` enforces - this — a server that exposes the mutation methods on `Topics` should - only grant them to operators who preserve the constraint. +* Give each category descriptor a BrowseName in a namespace owned by the + server. The store assigns that namespace to every alias `QualifiedName` it + reports. The materializer also moves `ns=0` BrowseNames out of the reserved + OPC Foundation namespace. Using a server-owned namespace keeps query results + and browsable nodes aligned, so `TranslateBrowsePathsToNodeIds` resolves + returned alias names. The reference server uses the diagnostics namespace + for all its descriptors. +* Part 17 §9 requires `Topics` aliases to target `PublishedDataSetType`. The + store and `AddAliasesToCategory` do not enforce this constraint. If a server + exposes mutation methods on `Topics`, grant access only to operators who + preserve it. -`Quickstarts.ReferenceServer` uses this in -`ReferenceServerConfigurationNodeManager`, which also creates the -`PublishedDataSet` instances and seeds the `Topics` aliases that point -at them — in the same place, so an alias can never target a dataset -that was not created. The nested `Devices` sub-category under -`TagVariables` is declared with the store seeding in -`ReferenceServer.ConfigureAliasNameStore`. +`Quickstarts.ReferenceServer` applies these rules in +`ReferenceServerConfigurationNodeManager`. The manager creates +`PublishedDataSet` instances and seeds the `Topics` aliases in the same +component. An alias therefore cannot target a dataset that the manager did +not create. `ReferenceServer.ConfigureAliasNameStore` seeds the nested +`Devices` sub-category under `TagVariables`. ### Custom backend @@ -397,11 +417,11 @@ public interface IAliasNameRefreshStrategy : IAsyncDisposable } ``` -Implementations watch for stale-cache triggers and invoke -`onInvalidate` whenever they detect a change. `MonitoredItemAliasNameRefreshStrategyOptions` -controls the underlying `Subscription`: it can be left owned (default -— created + deleted by the strategy) or set via `SharedSubscription` -to plug the monitored item into an externally managed subscription. +Implementations watch for stale-cache triggers and invoke `onInvalidate` +when they detect a change. `MonitoredItemAliasNameRefreshStrategyOptions` +controls subscription ownership. By default, the strategy creates and +deletes the underlying `Subscription`. Set `SharedSubscription` to use +an externally managed subscription for the monitored item. Disposing the resolver (`await using` / `DisposeAsync`) tears down the strategy: timer for polling, `MonitoredItem` + `Subscription` for the @@ -424,23 +444,23 @@ monitored-item variant. Disposal is idempotent and never throws. the OPC Foundation reserves for it (`Aliases.FindAliasVerbose` = `i=24054`, `TagVariables.LastChange` = `i=32854`, and so on). - Those identifiers are allocated in the standard identifier registry - (`StandardTypes.csv`) but are deliberately **not** declared in the - ModelDesign and **not** present in the published NodeSet — the - standard address space does not instantiate optional children. A - reserved id is a number set aside for the node *if* a server chooses - to expose it, not a node the server must have. Consequently the source - generator emits no parent-to-instance mapping and no `MethodIds` - constant for them, and `DiagnosticsNodeManager.ReservedChildIds` - supplies the nine method ids and the two missing `LastChange` ids - explicitly, each traceable to its registry row. The argument - properties do have generated `VariableIds` constants and are - referenced through those. + The standard identifier registry (`StandardTypes.csv`) allocates these + identifiers, but the ModelDesign does not declare the optional children + and the published NodeSet does not include them. The standard address + space therefore does not instantiate them. - Do not "fix" this by declaring the children in `StandardTypes.xml`: - that file is a verbatim copy of the OPC Foundation's ModelDesign and - is re-synced from upstream, so a local edit is lost on the next sync - and the NodeIds silently revert to factory-minted ones. + A reserved ID is available if a server chooses to expose a node; it does + not require the server to create that node. The source generator therefore + emits no parent-to-instance mapping or `MethodIds` constant for these + children. `DiagnosticsNodeManager.ReservedChildIds` supplies nine method + IDs and the two missing `LastChange` IDs explicitly, each traceable to its + registry row. The argument properties have generated `VariableIds` + constants, which the implementation uses to reference them. + + Do not add these children to `StandardTypes.xml`. That file copies the + OPC Foundation ModelDesign verbatim and is synchronized from upstream. + A local edit would be lost on the next sync, and the NodeIds would revert + to factory-minted values. * **`AliasNameCapabilities.AddAliasesToCategory` / `DeleteAliasesFromCategory`** — on an `AliasNameNodeManager` this defaults to `SecurityAdmin`-only via diff --git a/docs/AsyncServerSupport.md b/docs/AsyncServerSupport.md index ff05457078..56eff369eb 100644 --- a/docs/AsyncServerSupport.md +++ b/docs/AsyncServerSupport.md @@ -1,28 +1,48 @@ # Support of the TAP (Task Asynchronous Pattern) for server operations -The OPC UA .NET Standard stack has supported asynchronous operations for a long time. The asynchronous operations are based on the IAsyncResult pattern, which is also called the APM (Asynchronous Programming Model) or the Begin/End pattern. This pattern was introduced with .NET Framework 1.0 and is still supported in .NET 8.0. +The OPC UA .NET Standard stack has long supported asynchronous server +operations through the APM (Asynchronous Programming Model). APM uses +`IAsyncResult` and the Begin/End pattern. .NET Framework 1.0 introduced +this pattern, which remains supported in .NET 8.0. -In addition to the APM, the TAP (Task Asynchronous Pattern) is now also supported for server operations. The TAP is based on the Task and async/await keywords introduced in .NET Framework 4.0 and is the recommended way to implement asynchronous operations in modern .NET applications. +The stack also supports the TAP (Task Asynchronous Pattern) for server +operations. TAP uses `Task` and the `async`/`await` keywords introduced +in .NET Framework 4.0. It is the recommended way to implement asynchronous +operations in modern .NET applications. -In the future APM support will be deprecated, as it was never implemented for NodeManagers. +Server applications can implement node managers with TAP. This pattern +improves scalability by using server resources more efficiently. -The server library lets users implement Task based NodeManagers. -Implementing the TAP allows to improve the scalability of the server, as the TAP is significantly more efficient in terms of resource usage and performance. +The server library provides these TAP features: -In order to support the TAP pattern, the following changes have been made to the server library: +- Task-based `RequestQueue` and `TransportListenerCallback` +- Generated code and `MasterNodeManager` support Task-based operations. +- The `IAsyncNodeManager` interface supports fully asynchronous node managers. +- `AsyncNodeManagerAdapter` and `SyncNodeManagerAdapter` let synchronous + and asynchronous node managers run side by side. -- Introduce a Task based `RequestQueue` -- Introduce a Task based `TransportListenerCallback` -- Update the generated Code to support Task based operations -- Update of the `MasterNodeManager` to support Task based operations -- Introduce a Task based `IAsyncNodeManager` interface -- Introduce `AsyncNodeManagerAdapter` and `SyncNodeManagerAdapter` classes to support sync and async node managers side by side. +## Contents + +- [Upgrading an existing server](#upgrading-an-existing-server) +- [Async method calls](#async-method-calls) +- [AsyncCustomNodeManager](#asynccustomnodemanager) + - [Registering an AsyncCustomNodeManager](#registering-an-asynccustomnodemanager) + - [Async browse iteration](#async-browse-iteration) + - [Locking strategy vs CustomNodeManager2](#locking-strategy-vs-customnodemanager2) + - [1. Global write semaphore — `m_writeSemaphore`](#1-global-write-semaphore--m_writesemaphore) + - [2. Monitored-item semaphore — `m_monitoredItemSemaphore`](#2-monitored-item-semaphore--m_monitoreditemsemaphore) + - [3. Per-node locking for read and attribute access](#3-per-node-locking-for-read-and-attribute-access) + - [Monitored-item manager selection](#monitored-item-manager-selection) + - [Fully asynchronous change notifications](#fully-asynchronous-change-notifications) + - [Creating a custom node manager](#creating-a-custom-node-manager) ## Upgrading an existing server - Update `INodeManager.CreateMonitoredItems` to support the new `MonitoredItemIdFactory`. -- In a future release an `CustomNodeManagerAsync` class will be provided to simplify the creation of fully async NodeManagers. -- The existing `CustomNodeManager2` class can be used as is, and async operations can be implemented as needed using the different interfaces provided by the server library: +- `AsyncCustomNodeManager` is the recommended base class for fully + asynchronous node managers; see [AsyncCustomNodeManager](#asynccustomnodemanager). +- Keep using `CustomNodeManager2` and implement the asynchronous interfaces + your server needs: - `IAsyncNodeManager` for full async support - `ICallAsyncNodeManager` for async method calls - `IReadAsyncNodeManager` for async reading @@ -38,16 +58,24 @@ In order to support the TAP pattern, the following changes have been made to the - `IModifyMonitoredItemsAsyncNodeManager` for async monitored item modification - `ICreateMonitoredItemsAsyncNodeManager` for async monitored item creation ---> The MasterNodeManager automatically detects if a NodeManager implements any of the async interfaces and uses the async implementation if available. If no async interface is implemented, the sync implementation is used. +> `MasterNodeManager` detects which asynchronous interfaces a node manager +> implements. It uses an asynchronous implementation when available and +> otherwise falls back to the synchronous implementation. -- The Server already allows to register fully async NodeManagers, which implement the `IAsyncNodeManager` interface. To register a fully async Nodemanager use `StandardServer.RegisterNodeManager(IAsyncNodeManagerFactory)`. - For compatibility reasons the IAsyncNodeManager has a property `SyncNodeManager`, this needs to be implemented by passing your IAsyncNodeManager to the `SyncNodeManagerAdapter`. +- The server supports fully asynchronous node managers that implement + `IAsyncNodeManager`. Register one with + `StandardServer.RegisterNodeManager(IAsyncNodeManagerFactory)`. + `IAsyncNodeManager.SyncNodeManager` is required for compatibility; pass + the node manager to `SyncNodeManagerAdapter` to provide it. -## Async Method call +## Async method calls -Support for async method callbacks is already implemented by `CustomNodeManager2` to enable the support just add `IAsyncNodeManager` to your NodeManager implementation. -All generated code already has support for Async Methods e.g. `UpdateCertificateMethodState.OnCallAsync`. If the NodeManager implements `IAsyncNodeManager` the async callback is used automatically. -If a generic Method handler shall be used the `MethodState.OnCallMethod2Async` handler shall be used. +`CustomNodeManager2` supports asynchronous method callbacks. Implement +`IAsyncNodeManager` on your node manager to enable them. Generated method +state classes expose asynchronous callbacks such as +`UpdateCertificateMethodState.OnCallAsync`; the server uses these callbacks +when the node manager implements the interface. For a generic method handler, +use `MethodState.OnCallMethod2Async`. ## AsyncCustomNodeManager @@ -73,96 +101,65 @@ server.RegisterNodeManager(context => ### Async browse iteration -Every hook a custom node manager overrides is awaitable, and that includes browsing. -`AsyncCustomNodeManager.BrowseAsync` and `TranslateBrowsePathAsync` iterate an -`INodeBrowser` through `NextAsync(CancellationToken)` rather than `Next()`, so a browser that -has to reach an underlying system — the aggregation case, where the references come from -another server — awaits that fetch on the first `NextAsync` instead of holding a request -worker on a blocking call. The default `NextAsync` wraps `Next()`, so the browsers the stack -ships and every existing custom browser keep working untouched. See -[NodeManagers.md](NodeManagers.md#threading-contract-for-nodes-and-browsers) for the browser contract. +Every hook a custom node manager overrides is awaitable, including browse +operations. `AsyncCustomNodeManager.BrowseAsync` and +`TranslateBrowsePathAsync` iterate an `INodeBrowser` with +`NextAsync(CancellationToken)` instead of `Next()`. A browser can therefore +fetch data from an underlying system asynchronously. For example, it can +retrieve references from another server without occupying a request worker +with a blocking call. The default `NextAsync` wraps `Next()`, so the browsers +provided by the stack and existing custom browsers continue to work. +See [NodeManagers.md](NodeManagers.md#threading-contract-for-nodes-and-browsers) +for the browser contract. ### Locking strategy vs CustomNodeManager2 -`CustomNodeManager2` protects its entire address space with a **single coarse-grained monitor -lock** stored in the `Lock` property: +The synchronous `CustomNodeManager2` service paths use coarse-grained manager +synchronization. `AsyncCustomNodeManager` instead coordinates writes and +monitored-item management with awaitable semaphores while leaving node-state +synchronization inside `NodeState`. -```csharp -lock (Lock) -{ - // all reads and writes go through this single lock -} -``` - -While simple, this serialises all concurrent requests for the whole node manager and blocks the -calling thread, which prevents the use of `await` inside the critical section. - -`AsyncCustomNodeManager` replaces this with a **two-tier, await-compatible locking model**: +The following describes the implementation, not locks that application callers +should acquire. Use the manager's service, lifecycle, and fluent APIs so operation +admission, cancellation, and disposal remain coordinated. #### 1. Global write semaphore — `m_writeSemaphore` -A `SemaphoreSlim(1, 1)` that serialises all **write** operations across the node manager. -Because it is a `SemaphoreSlim` it can be acquired with `await`: - -```csharp -await m_writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); -try -{ - // safe to write any node -} -finally -{ - m_writeSemaphore.Release(); -} -``` - -Only one write request runs at a time, preventing concurrent modifications of the address space. -Read operations do **not** acquire this semaphore. +The manager serializes its Write service operations through an awaitable semaphore. +Read operations do not acquire it. The semaphore does not turn external updates or +several attribute accesses into a transaction; use the node APIs for state access. #### 2. Monitored-item semaphore — `m_monitoredItemSemaphore` -A second `SemaphoreSlim(1, 1)` that serialises all **monitored-item management** operations -(create, modify, delete, set-monitoring-mode, subscribe-to-events, condition-refresh, transfer). -This keeps subscription state consistent without blocking reads or writes: +A second `SemaphoreSlim(1, 1)` serializes **monitored-item management** +operations. It keeps subscription state consistent without blocking reads +or writes. The semaphore protects: -```csharp -await m_monitoredItemSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); -try -{ - // create / delete / modify monitored items -} -finally -{ - m_monitoredItemSemaphore.Release(); -} -``` +- create, modify, and delete operations +- monitoring-mode changes +- event subscriptions and condition refresh +- monitored-item transfers #### 3. Per-node locking for read and attribute access -Reads do not acquire any manager-wide lock. Instead they lock **only the individual `NodeState` -object** being accessed. This allows many reads to run truly in parallel across different nodes: +Reads do not acquire a manager-wide lock. `NodeState` synchronizes attribute and +collection access internally; the async manager awaits `ReadAttributeAsync` so that +asynchronous value handlers can complete without blocking a thread. -```csharp -lock (handle.Node) -{ - errors[ii] = handle.Node.ReadAttribute( - systemContext, - nodeToRead.AttributeId, - nodeToRead.ParsedIndexRange, - nodeToRead.DataEncoding, - value); -} -``` +**Do not use `lock (node)` or `lock (handle.Node)`.** Those monitors do not coordinate +with the node's private locks and cannot protect its state. Use the node's attribute, +child, and reference APIs instead. Individually synchronized operations do not form +an atomic multi-attribute transaction. -The same per-node lock is used for the old-value read inside `WriteAsync` and for -`FindChildBySymbolicName` lookups in the component cache. +See the [threading contract for nodes and browsers](NodeManagers.md#threading-contract-for-nodes-and-browsers) +for supported operations, snapshot boundaries, and browser ownership. | Concern | `CustomNodeManager2` | `AsyncCustomNodeManager` | |----------------------------------|----------------------------|------------------------------------| -| Address-space reads | Global `lock (Lock)` | Per-node `lock (node)` (parallel) | -| Address-space writes | Global `lock (Lock)` | `await m_writeSemaphore` (serial) | -| Monitored-item management | Global `lock (Lock)` | `await m_monitoredItemSemaphore` | -| `await` inside critical section | Not possible | Supported everywhere | +| Address-space reads | Manager synchronization | Internally synchronized node APIs | +| Write service operations | Manager synchronization | Awaitable write serialization | +| Monitored-item management | Manager synchronization | Awaitable monitored-item serialization | +| Awaiting asynchronous handlers | Use the asynchronous path | Supported by asynchronous operations | | Implemented interface | `INodeManager3` | `IAsyncNodeManager` | ### Monitored-item manager selection @@ -190,7 +187,10 @@ public MyNodeManager(IServerInternal server, ApplicationConfiguration config) ### Fully asynchronous change notifications -`AsyncCustomNodeManager` propagates value changes and events to monitored items over a fully asynchronous push path, so no thread is blocked when a node exposes an asynchronous value read handler or when the per-node notification channel applies back-pressure. +`AsyncCustomNodeManager` sends value changes and events to monitored items +through an asynchronous push path. This path does not block a thread when a +node uses an asynchronous value-read handler or when the per-node notification +channel applies back-pressure. `NodeState` exposes asynchronous counterparts of its change-notification API alongside the existing synchronous members: @@ -201,11 +201,30 @@ public MyNodeManager(IServerInternal server, ApplicationConfiguration config) | `ReportEvent(context, e)` | `ReportEventAsync(context, e, ct)` | | `OnReportEvent` | `OnReportEventAsync` | -The additions are non-breaking: existing synchronous callers and sinks continue to work unchanged. `MonitoredNodeMonitoredItemManager` wires the asynchronous sinks, so after a successful write `AsyncCustomNodeManager.WriteAsync` flushes changes with `await handle.Node.ClearChangeMasksAsync(...)`. The value for each monitored attribute is read at that moment through `NodeState.ReadAttributeAsync` — honoring an asynchronous `OnReadValueAsync` / `OnSimpleReadValueAsync` handler when one is registered — and the resulting snapshot is enqueued by awaiting the bounded per-node channel. Because the read is materialized before the change is enqueued, every notification carries the node value at the instant of the change (an enqueue-time snapshot); because both the read and the enqueue are awaited, the producing thread is never blocked. +These additions do not break existing synchronous callers or sinks. +`MonitoredNodeMonitoredItemManager` wires the asynchronous sinks. After a +successful write, `AsyncCustomNodeManager.WriteAsync` flushes changes by +awaiting `handle.Node.ClearChangeMasksAsync(...)`. -The synchronous `ClearChangeMasks` / `ReportEvent` still drive the asynchronous sinks: they complete inline when the node is synchronously readable and the channel has capacity, and block the calling thread only when a genuinely asynchronous read is in flight or the channel is full. A synchronous node manager therefore keeps working without any change, paying a blocking cost only in those two cases. +The asynchronous sink reads each monitored attribute through +`NodeState.ReadAttributeAsync`. When registered, an asynchronous +`OnReadValueAsync` or `OnSimpleReadValueAsync` handler supplies the value. The +sink then enqueues that snapshot through the bounded per-node channel and +awaits completion. It reads the value before enqueueing the change, so each +notification captures the node value at the time of the change. Awaiting both +operations keeps the producing thread from blocking. -Overflow and discard — the FIFO/LIFO `DiscardOldest` behavior and the `Overflow` status bit — remain a property of each monitored item's own queue. The shared per-node channel is a lossless conduit and does not drop notifications, so a monitored item's configured queue policy is always applied to the full stream of changes. +The synchronous `ClearChangeMasks` and `ReportEvent` methods also drive the +asynchronous sinks. They complete inline when the node is synchronously +readable and the channel has capacity. They block the calling thread only +while an asynchronous read is in flight or the channel is full. Synchronous +node managers continue to work without changes, but may block in those two +cases. + +Each monitored item's queue controls overflow and discard behavior, +including FIFO/LIFO `DiscardOldest` and the `Overflow` status bit. The shared +per-node channel does not drop notifications. As a result, the configured +queue policy applies to the full stream of changes. ### Creating a custom node manager diff --git a/docs/AuthorizationService.md b/docs/AuthorizationService.md index de5ea6df8e..7dc2efaa01 100644 --- a/docs/AuthorizationService.md +++ b/docs/AuthorizationService.md @@ -1,10 +1,26 @@ # AuthorizationService Developer Guide -OPC 10000-12 §9 defines the **AuthorizationService** — a GDS service that issues access tokens for -OPC UA applications. Part 12 v1.05 uses the two-phase `StartRequestToken` / `FinishRequestToken` -flow; `RequestAccessToken` remains available only for legacy compatibility. +Open Platform Communications Unified Architecture (OPC UA) Part 12 §9 +defines the **AuthorizationService**, which a Global +Discovery Server (GDS) provides to issue access tokens for OPC UA +applications. Part 12 v1.05 uses the two-phase `StartRequestToken` / +`FinishRequestToken` flow. `RequestAccessToken` remains available for +legacy compatibility. -For client-side JWT use during `ActivateSession`, see [Identity Providers](IdentityProviders.md). +For client-side JSON Web Token (JWT) use during `ActivateSession`, see +[Identity Providers](IdentityProviders.md). + +## Contents + +- [Architecture](#architecture) +- [Modern Part 12 v1.05 flow](#modern-part-12-v105-flow) + - [Refreshing an access token](#refreshing-an-access-token) +- [Server hosting](#server-hosting) + - [Custom `ITokenIssuer` for cloud or HSM signing](#custom-itokenissuer-for-cloud-or-hsm-signing) +- [Server-side abstraction](#server-side-abstraction) +- [Client identity-provider bridge](#client-identity-provider-bridge) +- [Legacy compatibility](#legacy-compatibility) +- [Audit events](#audit-events) ## Architecture @@ -25,14 +41,21 @@ For client-side JWT use during `ActivateSession`, see [Identity Providers](Ident └──────────────────────────┘ ``` -`Opc.Ua.Gds.Server.IAccessTokenProvider` is the server-side request-routing abstraction behind the -AuthorizationService method handlers. It validates resource ids, policies, pending request ids, and user -token inputs before the service returns a token to the caller. Modern Part 12 v1.05 clients call -`StartRequestToken` followed by `FinishRequestToken`; legacy clients can still call `RequestAccessToken`, -which dispatches to `IAccessTokenProvider.RequestAccessTokenAsync` and is marked `[Obsolete]` in the API. -`Opc.Ua.Identity.ITokenIssuer` is the signing back-end used by the provider once a request is accepted. -The default issuer signs JWTs with local certificate material, while custom issuers can delegate signing -to a cloud KMS, HSM, or corporate authorization service without changing the OPC UA method wiring. +`Opc.Ua.Gds.Server.IAccessTokenProvider` routes requests for the +AuthorizationService method handlers. Before returning a token, the provider +validates resource IDs, policies, pending request IDs, and user-token inputs. + +Modern Part 12 v1.05 clients call `StartRequestToken` followed by +`FinishRequestToken`. Legacy clients can still call `RequestAccessToken`; +the provider dispatches that call to +`IAccessTokenProvider.RequestAccessTokenAsync`. The API marks this method +`[Obsolete]`. + +`Opc.Ua.Identity.ITokenIssuer` signs a token after the provider accepts a +request. The default issuer uses local certificate material. Custom issuers +can delegate signing to a cloud key-management service, a hardware security +module, or a corporate authorization service without changing the OPC UA +method wiring. ## Modern Part 12 v1.05 flow @@ -56,7 +79,11 @@ var (jwt, expiresAt, refreshToken, refreshExpiresAt) = new SignatureData()); ``` -`StartRequestToken` validates the requested audience and scopes, allocates a continuation request id, and stores the pending request in memory. `FinishRequestToken` exchanges that id for a compact JWT (`tokenType = "JWT"`) signed by the configured `Opc.Ua.Identity.ITokenIssuer`. +`StartRequestToken` validates the requested audience and scopes. It allocates +a continuation request ID and stores the pending request in memory. +`FinishRequestToken` exchanges that ID for a compact JWT +(`tokenType = "JWT"`) signed by the configured +`Opc.Ua.Identity.ITokenIssuer`. ### Refreshing an access token @@ -105,14 +132,25 @@ builder.WithAuthorizationService(o => }); ``` -`MyTokenIssuer` implements `Opc.Ua.Identity.ITokenIssuer`. The default `CertificateJwtIssuer` signs hand-rolled RFC 7515 JWS tokens with ECDSA (`ES256`/`ES384`/`ES512`) or RSA (`RS256` default; RSA-PSS supported by custom issuers) without adding a JWT package dependency. +`MyTokenIssuer` implements `Opc.Ua.Identity.ITokenIssuer`. The default +`CertificateJwtIssuer` signs JSON Web Signature (JWS) tokens defined by RFC +7515. It uses the Elliptic Curve Digital Signature Algorithm (ECDSA) +(`ES256`, `ES384`, or `ES512`) or RSA (`RS256` by default). Custom issuers +can also use RSA-PSS. The default issuer does not add a JWT package +dependency. ### Custom `ITokenIssuer` for cloud or HSM signing Use `WithAuthorizationService()` when the signing key is not -resident in the process (for example Azure Key Vault, AWS KMS, an HSM, or -a corporate token service). The issuer builds the JWS header/payload and -delegates the signing operation to the external service. +resident in the process. External signing options include: + +- Azure Key Vault +- AWS KMS +- An HSM +- A corporate token service + +The issuer builds the JWS header and payload, then delegates the signing +operation to the external service. ```csharp public sealed class CloudKmsTokenIssuer : ITokenIssuer @@ -153,8 +191,8 @@ services.AddOpcUa() ``` The `SigningCertificate` option still advertises the issuer identity and -key material expected by local verifiers; the custom issuer decides how -the actual signature is produced. +key material that local verifiers expect. The custom issuer controls how it +produces the signature. ## Server-side abstraction @@ -176,7 +214,13 @@ The default `AuthorizationServiceManager` delegates to `InMemoryAccessTokenProvi ## Legacy compatibility -`RequestAccessToken` and `AuthorizationServiceClient.RequestAccessTokenAsync` are marked obsolete because Part 12 v1.05 replaced the one-shot exchange with `StartRequestToken` / `FinishRequestToken`. The wire method is still dispatched to `IAccessTokenProvider.RequestAccessTokenAsync` so v1.04 clients continue to work when a provider is configured. +`RequestAccessToken` and +`AuthorizationServiceClient.RequestAccessTokenAsync` are obsolete because +Part 12 v1.05 replaced the one-shot exchange with `StartRequestToken` and +`FinishRequestToken`. When a provider is configured, the service still +dispatches the wire method to +`IAccessTokenProvider.RequestAccessTokenAsync` so v1.04 clients continue to +work. ## Audit events diff --git a/docs/Benchmarks.md b/docs/Benchmarks.md index ed0e5c0e11..70468fbfb1 100644 --- a/docs/Benchmarks.md +++ b/docs/Benchmarks.md @@ -1,10 +1,32 @@ # Performance Benchmarks -This document covers the performance of the **2.0** stack: how to run the -BenchmarkDotNet harnesses, the **2.0 vs 1.5.378** comparison (what improved, what is -still slower, and why, plus planned future work), the **pooled encodeable** -(subscription-notification pooling) micro-benchmarks, and **server session scalability** -(concurrent-session capacity vs 1.5.378). +This document explains how to run the **2.0** stack's BenchmarkDotNet +harnesses and summarizes its performance against **1.5.378**. It covers +improvements, remaining slowdowns, and planned future work. It also reports +the pooled encodeable micro-benchmarks and server session scalability. + +## Contents + +- [How to run](#how-to-run) +- [2.0 vs 1.5.378](#20-vs-15378) + - [Why 2.0 differs](#why-20-differs-from-15378) + - [What was measured](#what-was-measured-20-vs-15378-409-matched-benchmarks) + - [What improved](#what-improved-in-20-vs-15378-and-why) + - [What is still slower](#what-is-still-slower-in-20-vs-15378-and-why) + - [Future work](#future-work) + - [Environment and caveats](#environment-and-caveats) +- [Pooled encodeable](#pooled-encodeable) + - [Source](#source) + - [Methodology](#methodology) + - [Environment](#environment) + - [Results](#results) + - [Interpretation](#interpretation) + - [Reproducing](#reproducing) + - [Out of scope](#out-of-scope) +- [Server session scalability](#server-session-scalability) + - [Observed scaling](#observed-scaling) + - [Subscription transport buffer pooling](#subscription-transport-buffer-pooling) + - [Sizing and configuration](#sizing-and-configuration) ## How to run @@ -30,30 +52,32 @@ across every security policy. ## 2.0 vs 1.5.378 -This section compares the **2.0** stack against the previous **1.5.378** release on -.NET 10, explains **what improved and why**, **what is still slower and why**, and the -**future work** planned to close the remaining gaps. +This section compares the **2.0** stack with **1.5.378** on .NET 10. It +explains what improved, what remains slower, and what future work could +close the gaps. All ratios below are **2.0 ÷ 1.5.378**. **< 1.0 means 2.0 is faster / allocates less; > 1.0 means 2.0 is slower / allocates more.** -It is intended as living documentation: when a perf-sensitive change lands, update -the relevant section and re-run the affected benchmark class (see +Keep this section current as performance-sensitive changes land. Update the +relevant results and rerun the affected benchmark class (see [How to run](#how-to-run)). ### Why 2.0 differs from 1.5.378 -2.0 is a deliberate redesign that, among many other things, replaced several -reference types with **`readonly struct` value types** (`Variant`, `DataValue`, -`NodeId`, `ExpandedNodeId`, `ByteString`, `ArrayOf`, `DateTimeUtc`) and moved -the client/server onto a fully `async` (TAP) pipeline. That redesign **reduces -allocations and GC pressure** (fewer heap objects, less Gen2 traffic) but trades -some of that for **higher per-access CPU cost** — every property read on a large -`readonly struct` may copy the struct or re-run a discriminated-union switch. +The 2.0 redesign replaced several reference types with **`readonly struct` +value types**: `Variant`, `DataValue`, `NodeId`, `ExpandedNodeId`, +`ByteString`, `ArrayOf`, and `DateTimeUtc`. It also moved client and server +operations to a fully `async` (TAP) pipeline. -The benchmarks below quantify that trade-off: 2.0 allocates less and is faster on -most client paths, at the cost of some encode/decode CPU on the value-type hot -paths. +The redesign **reduces allocations and GC pressure** by creating fewer heap +objects and less Gen2 traffic. It can also increase per-access CPU cost. +Reading a property from a large `readonly struct` may copy the struct or rerun +a discriminated-union switch. + +The benchmarks below quantify this trade-off. Version 2.0 allocates less and +runs faster on most client paths, but some value-type encoding and decoding +paths use more CPU. ### What was measured (2.0 vs 1.5.378, 409 matched benchmarks) @@ -62,12 +86,12 @@ paths. two areas only: the **binary encoder** and **session establishment**. Everything else is at parity or faster. -> The two aggregate geomeans above are from the **baseline full-job** 2.0-vs-1.5.378 -> sweep (409 matched benchmarks). Several areas have since improved further (binary -> decode allocation, in-memory and external-stream JSON — see the per-area rows below); -> the full sweep was not re-run on the final tree, so the per-area table reflects the -> current state while the aggregate is the documented baseline (the improvements only -> move it further in 2.0's favour). +> The two aggregate geomeans above come from the **baseline full-job** +> 2.0-versus-1.5.378 sweep of 409 matched benchmarks. Several areas have since +> improved, including binary decode allocation and in-memory and external-stream +> JSON (see the per-area rows below). The team did not rerun the full sweep on +> the final tree. The per-area table shows current results, while the aggregate +> remains the documented baseline. These improvements favor 2.0 further. | Area | time | alloc | |---|--:|--:| @@ -78,14 +102,16 @@ paths. | Binary encode (`BinaryEncoderBenchmarks`) | still slower¹ | ~parity¹ | | Session establishment (`SecurityPolicyBenchmarks`) | still slower³ | — | -¹ The encoder still trails 1.5.378 on **time** (intrinsic value-type struct cost — see -below). Its **allocation** is unchanged by the codec work: a ground-truth -`GC.GetAllocatedBytesForCurrentThread` A/B over the benchmark payload shows identical -per-op allocation before/after the `BinaryPrimitives` change (e.g. `ArraySegmentStream` -76,808 B/op in both). The `Allocated` column that BenchmarkDotNet reports for the -`ArrayPool`/`BufferManager`-backed stream variants is a pooled-buffer **accounting -artifact** (pool rentals/returns attributed differently across separate runs) and is not a -reliable per-op figure for those variants. +¹ The encoder remains slower than 1.5.378 on **time**, consistent with the +intrinsic cost of value-type structs (see below). The codec work did not change +**allocation**. A `GC.GetAllocatedBytesForCurrentThread` A/B test on the +benchmark payload measured the same allocation before and after the +`BinaryPrimitives` change: 76,808 B/op for `ArraySegmentStream`. + +BenchmarkDotNet's `Allocated` values for `ArrayPool`/`BufferManager`-backed +streams are an accounting artifact. Pool rentals and returns can be attributed +differently across independent runs, so these values are not reliable per-op +figures. ² External-stream JSON allocation improved from ~1.44× to ~1.29× once the `Utf8JsonWriter` is pooled/reused (below); the residual is inherent UTF-8 transcoding. ³ Session establishment remains the largest single regression vs 1.5.378; this is @@ -186,15 +212,17 @@ bulk numeric-array copies described under [what improved](#binary-decoding-31-le #### JSON encoder, external-stream — improved by `Utf8JsonWriter` pooling -The external-stream JSON variants (`JsonEncoderBenchmarks`, which write to a caller -stream) trailed 1.5.378 because each encoder allocated a fresh `Utf8JsonWriter` and -pays `Utf8JsonWriter` UTF-8 transcoding (vs 1.5.378's `StreamWriter`). 2.0 now -**pools and reuses `Utf8JsonWriter` instances** for the external-stream and -external-`IBufferWriter` constructors: a writer is rented from a small pool (keyed by -`Indented`, capped) and re-targeted with `Utf8JsonWriter.Reset(...)` instead of being -allocated per encoder, then returned on dispose. This removes the per-encoder writer -allocation; the residual is the inherent UTF-8 transcoding. The in-memory path -remains pooled via the `ArrayPool` buffer described above. +The external-stream JSON variants in `JsonEncoderBenchmarks` write to a +caller-provided stream. They trailed 1.5.378 because each encoder allocated a +new `Utf8JsonWriter` and paid for UTF-8 transcoding (1.5.378 used +`StreamWriter`). + +Version 2.0 now **pools and reuses `Utf8JsonWriter` instances** for external +stream and `IBufferWriter` constructors. The pool is small, keyed by +`Indented`, and capped. Each encoder rents a writer, retargets it with +`Utf8JsonWriter.Reset(...)`, and returns it on disposal. This removes the +per-encoder writer allocation; UTF-8 transcoding remains. The in-memory path +continues to use the `ArrayPool` buffer described above. #### Session establishment — still slower; dominated by discovery + crypto @@ -211,9 +239,10 @@ the security policies common to both 1.5.378 and 2.0 — for the true apples-to- change — **now exists** (`SecurityPolicySessionCommonWithV15378Benchmarks`, covering Basic128Rsa15, Basic256, Basic256Sha256, Aes128_Sha256_RsaOaep, Aes256_Sha256_RsaPss). -An allocation profile of one **full connect including discovery** (Basic256Sha256) shows -~3.2 MB/op dominated by **per-connect endpoint discovery** (`GetEndpointsAsync`) plus -strings / `Char[]` / XML / `Byte[]` materialization: +An allocation profile of one **full connect, including discovery** +(`Basic256Sha256`) shows about **3.2 MB/op**. **Per-connect endpoint +discovery** through `GetEndpointsAsync` accounts for most of the allocation. +The profile attributes it to: | Allocator (Basic256Sha256 connect) | ~bytes/op | |---|--:| @@ -230,15 +259,16 @@ regression and was deliberately not pursued. Session establishment is a one-time-per-connection cost, so absolute throughput impact is bounded to connect churn. -> **Benchmark fidelity:** because real applications cache the discovered endpoints, -> `SecurityPolicyBenchmarks.CreateCloseSessionAsync` / -> `SessionLifecycleWithReadAsync` pass the cached `Endpoints` into `ConnectAsync`, so -> they measure pure session **create / activate / close** rather than re-running -> `GetEndpointsAsync` on every iteration. A separate `DiscoverEndpointsAsync` -> benchmark tracks endpoint discovery in isolation. A first low-risk discovery -> reduction has landed (`DiscoveryClient.PatchEndpointUrls` skips redundant endpoint-URL -> rewrites, ~22 KB/op); the bulk of the remaining discovery cost is intrinsic -> endpoint / user-token / application-description materialization and stays future work. +> **Benchmark fidelity:** real applications cache discovered endpoints. +> `SecurityPolicyBenchmarks.CreateCloseSessionAsync` and +> `SessionLifecycleWithReadAsync` pass cached `Endpoints` to `ConnectAsync`. +> These benchmarks create, activate, and close sessions without rerunning +> `GetEndpointsAsync` on every iteration. `DiscoverEndpointsAsync` measures +> endpoint discovery separately. +> `DiscoveryClient.PatchEndpointUrls` skips redundant endpoint-URL rewrites +> and reduces allocation by about 22 KB/op. Most remaining discovery cost +> comes from materializing endpoints, user-token policies, and application +> descriptions; that work remains future work. #### `JsonEncoderTests.ServiceMessageContext` — ~2× on a tiny absolute @@ -250,24 +280,28 @@ work above. Low priority. ### Future work -1. **Binary encoder — drive time to 1.5.378 parity.** Scalar writes already go through - `BinaryPrimitives` and numeric arrays bulk-blit (allocation is at parity). The remaining - gap is **time**, and a micro-profile localises it to the `Variant` value-type access path: - inside `WriteVariantValue` the `builtInType` switch already knows the type, but the - `Variant.GetXxx()` accessors then call `TryGetValue`/`TryGetScalar`, which re-read - `TypeInfo` and re-check scalar/type before returning the union value (scalar writes are - ~60–160 ns/op vs ~5–7 ns/op for the raw primitive write; `StatusCode`/`LocalizedText`/ - `Guid`/`NodeId` are slowest). A potential win is `internal` "already-validated" union - accessors the encoder switch can call to skip the re-check — but an initial snapshot attempt - showed no clear measured win, so this is genuinely close to the intrinsic value-type cost and - should be re-attempted only with a profile-proven gain. -2. **Session establishment — discovery materialization.** An apples-to-apples session - benchmark restricted to the security policies common to both 1.5.378 and 2.0 exists - (`SecurityPolicySessionCommonWithV15378Benchmarks`). The remaining work is to further - reduce the endpoint/user-token/app-description materialization and XML reader/writer - allocation in `GetEndpointsAsync` on the connect path (a first low-risk reduction has - landed; the bulk of the cost is intrinsic description materialization), and optionally - cache `ConfiguredEndpoint` so repeat connects skip discovery. +1. **Binary encoder — drive time to 1.5.378 parity.** Scalar writes use + `BinaryPrimitives`, and numeric arrays bulk-blit; allocation is at parity. + The remaining gap is **time**. A micro-profile points to the `Variant` + value-type access path. Inside `WriteVariantValue`, the `builtInType` + switch already knows the type, but `Variant.GetXxx()` calls + `TryGetValue`/`TryGetScalar`, which reread `TypeInfo` and check the scalar + and type before returning the union value. Scalar writes take about + 60–160 ns/op, compared with 5–7 ns/op for a raw primitive write. + `StatusCode`, `LocalizedText`, `Guid`, and `NodeId` are slowest. + + Internal, already-validated union accessors might let the encoder skip + these checks. An initial attempt showed no clear measured gain. Revisit + this only if profiling demonstrates an improvement over the intrinsic + value-type cost. +2. **Session establishment — discovery allocations.** The + `SecurityPolicySessionCommonWithV15378Benchmarks` benchmark compares only + security policies supported by both 1.5.378 and 2.0. Further work could + reduce allocations as `GetEndpointsAsync` builds endpoint, user-token, and + application-description data and processes XML. A low-risk reduction has + already landed, but most remaining cost comes from building the endpoint + description. Caching `ConfiguredEndpoint` could also let repeat connections + skip discovery. ### Environment and caveats @@ -280,12 +314,12 @@ work above. Low priority. - The 1.5.378 baseline is a full-job run; the per-class encoder refresh on 2.0 is a faster, higher-variance ShortRun, so treat those ratios as directional. Non-encoder classes carry run-to-run variance. -- **Pooled-stream allocation columns are unreliable:** for the `ArrayPool`/`BufferManager`- - backed stream variants (`ArraySegmentStream`, etc.), BenchmarkDotNet's `Allocated` - column attributes pool rentals/returns differently across separate runs, so it is **not** - a reliable per-op figure and must not be compared across two independent runs. Use a - ground-truth `GC.GetAllocatedBytesForCurrentThread` A/B for those (as was done to confirm - the binary-encoder allocation is unchanged by the `BinaryPrimitives` work). +- **Pooled-stream allocation columns are unreliable.** For `ArrayPool`- and + `BufferManager`-backed streams such as `ArraySegmentStream`, BenchmarkDotNet + can attribute pool rentals and returns differently across independent runs. + Do not compare these columns as per-op figures. Use a + `GC.GetAllocatedBytesForCurrentThread` A/B test instead, as in the + binary-encoder comparison above. - **Legacy target frameworks:** 2.0 perf optimization targets the modern runtime (.NET 10). The **.NET Framework (net48)** and **netstandard2.0** target frameworks are **not** a performance optimization target for 2.0: where a fast path is gated on `NET6_0_OR_GREATER` @@ -396,7 +430,23 @@ activator pool system. ## Server session scalability -The `[Explicit]` macro test `ServerManySessionsLoadTestAsync(int sessionCount)` (`tests/Opc.Ua.Sessions.Tests/LoadTest.cs`) exercises the reference server under many concurrent sessions. Each session opens its own secure channel, creates one slow-publishing subscription (1000 ms) with a single monitored item on a shared value node, and a separate writer session changes that value periodically; every session is expected to receive value-change notifications over a steady-state window. It runs over `Basic256Sha256` (sign & encrypt) and asserts that all sessions connect and all receive notifications. It is parameterized from a `500` baseline up to a `10000` stress case (500, 1000, 1500, 2000, 2500, 4000, 5000, 8000, 10000), selected by name (e.g. `ServerManySessionsLoadTestAsync(2000)`). +The `[Explicit]` macro test +`ServerManySessionsLoadTestAsync(int sessionCount)` in +`tests/Opc.Ua.Sessions.Tests/LoadTest.cs` exercises the reference server with +many concurrent sessions. Each session opens a secure channel and creates a +slow-publishing subscription (1000 ms) with one monitored item on a shared +value node. A separate writer session changes that value periodically. The +test checks that every session receives value-change notifications during a +steady-state window. It uses `Basic256Sha256` (sign and encrypt) and requires +all sessions to connect and receive notifications. + +The test offers these session counts: + +- 500 (baseline) +- 1000, 1500, 2000, 2500 +- 4000, 5000, 8000, 10000 (stress case) + +Select a case by name, for example `ServerManySessionsLoadTestAsync(2000)`. > For a deep, code-referenced analysis of *why* a single node tops out here — the establishment vs steady-state boundaries and the built-in controls for degrading gracefully under load — see [Server Session Scalability](ServerScalability.md). @@ -411,9 +461,27 @@ The `[Explicit]` macro test `ServerManySessionsLoadTestAsync(int sessionCount)` ### Observed scaling -On comparable hardware **1.5.378** capped a single node at roughly **~2000 concurrent sessions**: every held long-poll `Publish` pinned a request-processing worker, so the worker pool - not CPU - became the bottleneck as the session count climbed. **2.0** decouples held Publishes from the worker budget (`ServerConfiguration.DecoupleHeldPublishRequests`, on by default), so a small worker pool serves many thousands of parked Publishes and the ceiling moves out to session establishment (the CPU-bound RSA handshake) - about **~4000** concurrent sessions on the 6-core machine below. +On comparable hardware, **1.5.378** capped a single node at about **2000 +concurrent sessions**. Each held long-poll `Publish` pinned a request +processing worker, so the worker pool—not the CPU—became the bottleneck as +sessions increased. -The numbers below were measured on an **Intel Xeon W-2235 (6 physical cores / 12 logical threads), 64 GB RAM, 64-bit Windows**, with the client and the in-process reference server running in the *same* process on a shared developer machine under light background load. Because both ends share the same cores, session establishment - a CPU-bound `Basic256Sha256` RSA handshake performed on both the client and the server - is the dominant cost, and its throughput declines as concurrency rises. Steady-state publish delivery is serviced by a per-cycle sweep parallelized across cores; for every count that established cleanly, all sessions received 100 % of their notifications within the steady-state window (0 drops). +Version **2.0** decouples held Publishes from the worker budget by default +(`ServerConfiguration.DecoupleHeldPublishRequests`). A small worker pool can +therefore serve many parked Publishes. The limit shifts to session +establishment, where CPU-bound RSA handshakes support about **4000** sessions +on the six-core machine below. + +These measurements used an **Intel Xeon W-2235 (6 physical cores / 12 logical +threads), 64 GB RAM, 64-bit Windows**. The client and in-process reference +server shared a developer machine under light background load. Both ends +competed for the same cores, so `Basic256Sha256` RSA handshakes on the client +and server dominated establishment time. Throughput declined as concurrency +increased. + +A per-cycle sweep parallelized across cores delivered steady-state Publishes. +For every count that established successfully, all sessions received 100% of +their notifications within the steady-state window (0 drops). | Concurrent sessions | Sessions establish | Average sessions/sec created | All sessions receive notifications | | --- | --- | --- | --- | @@ -425,7 +493,23 @@ The numbers below were measured on an **Intel Xeon W-2235 (6 physical cores / 12 | 4000 | Yes | 11 | Yes | | 10000 | No (*) | — | — | -(*) These figures are hardware- and load-dependent and are directional; they improve on dedicated hardware where the client and server run on separate machines. With `ServerConfiguration.DecoupleHeldPublishRequests` on (the default) and `MaxRequestThreadCount` sized to the active establishment concurrency (~200) rather than to the session count, this 6-core machine cleanly established every session and delivered 100 % of notifications up to **~4000** concurrent sessions. At 10000 it tips into a secure-channel connect storm (repeated handshake aborts/retries and tens of thousands of channel attempts for the target) and does not establish the full set within the connect budget - so on this hardware establishment, not notification delivery, is the ceiling; more cores push it higher. When sessions establish but do not receive notifications the last column instead reads `No (N drops)`, where `N` is the number of sessions that did not receive notifications within the window. See [Server Session Scalability](ServerScalability.md) for the code-referenced analysis and the built-in controls for degrading gracefully under load. +(*) These figures depend on hardware and load. They are directional and may +improve on dedicated hardware with separate client and server machines. + +Enable `ServerConfiguration.DecoupleHeldPublishRequests` (the default) and +set `MaxRequestThreadCount` to active session-establishment concurrency +(about 200). On this six-core machine, that setup established every session +and delivered 100% of notifications up to **~4000** concurrent sessions. +At 10000 sessions, repeated +handshake aborts and retries caused a secure-channel connect storm, with tens +of thousands of channel attempts. The server did not establish all sessions +within the connect budget. On this machine, session establishment—not +notification delivery—limits capacity; more cores can raise the limit. + +If sessions connect but do not receive notifications, the last table column +shows `No (N drops)`. `N` is the number of sessions that missed notifications +during the window. See [Server Session Scalability](ServerScalability.md) for +the code-referenced analysis and controls for graceful degradation under load. ### Subscription transport buffer pooling @@ -433,14 +517,42 @@ The numbers below were measured on an **Intel Xeon W-2235 (6 physical cores / 12 Channels now normalize their advertised send and receive limits to the largest protocol-safe value that keeps the data plus cookie in the same `ArrayPool` bucket (while preserving the OPC UA 8,192-byte minimum), and both TCP transport creation paths receive that normalized limit. A configured value of either 65,535 or 65,536 consequently uses 65,536-byte arrays instead of 131,072-byte arrays. -The explicit net10.0 harness `SubscriptionArrayPoolDiagnosticsLoadTests` (`tests/Opc.Ua.Sessions.Tests/SubscriptionArrayPoolDiagnosticsLoadTests.cs`) measures real subscription traffic with 255 and 257 session Publish queues (immediately below and above the parallel Publish threshold) plus a slow-consumer case. Across both configured buffer sizes, all measured scenarios used only the 65,536-byte bucket, rents and returns balanced exactly after quiescence, outstanding buffers returned to zero, peak outstanding buffers stayed between 528 and 536, and the measurement windows triggered no Gen 2 collections. The 500-session regression baseline also completed for `opc.tcp`, `https`, and `opc.https`, with all 500 sessions receiving notifications. +The explicit net10.0 harness +`SubscriptionArrayPoolDiagnosticsLoadTests` +(`tests/Opc.Ua.Sessions.Tests/SubscriptionArrayPoolDiagnosticsLoadTests.cs`) +measures real subscription traffic in three cases: 255 and 257 session +Publish queues (just below and above the parallel Publish threshold), plus a +slow consumer. + +Across both configured buffer sizes: + +- Every scenario used only the 65,536-byte bucket. +- Rent and return counts balanced after quiescence. +- Outstanding buffers returned to zero; peak outstanding buffers ranged from + 528 to 536. +- No Gen 2 collections occurred during measurement. + +The 500-session regression baseline also completed for `opc.tcp`, `https`, +and `opc.https`. All 500 sessions received notifications. The net10.0 in-process short `BufferManagerBenchmarks` run completed 256 `BufferManager.TakeBuffer`/`ReturnBuffer` pairs in 56.18 us versus 52.52 us for direct `ArrayPool.Shared` use, with no managed allocation reported for either pooled path. These short-run timings are directional; the subscription harness is the authoritative ownership and bucket-size check. ### Sizing and configuration * `MaxSessionCount` (default 100) caps the concurrent open sessions; size `MaxChannelCount` (one channel per session) and `MaxSubscriptionCount` above the target session count. -* `MaxRequestThreadCount` caps concurrent request processing. With `ServerConfiguration.DecoupleHeldPublishRequests` on (the default), a held long-polled `Publish` releases its worker at the park point instead of occupying it for the whole wait, so the pool no longer has to scale with the session count - size it to the active (non-parked) establishment concurrency (a small pool of ~100–200 workers cleanly served thousands of sessions in measurement). Sizing it to the session count is counterproductive: a very large pool oversubscribes the cores during the connect burst, slowing establishment and lowering the ceiling. `MinRequestThreadCount` pre-warms the pool so a connect burst is not throttled by thread-pool cold-start. (Setting `DecoupleHeldPublishRequests = false` restores the legacy coupling, where a server serving N sessions again needs a pool well above N or held `Publish` requests starve other services with `BadRequestTimeout`.) +* `MaxRequestThreadCount` caps concurrent request processing. With + `ServerConfiguration.DecoupleHeldPublishRequests` enabled (the default), a + held long-poll `Publish` releases its worker while it waits. Set + `MaxRequestThreadCount` for the number of sessions being established at + once, not the total number of open sessions. In these measurements, + 100–200 workers served thousands of sessions. A very large pool + oversubscribes cores during connection bursts and slows establishment. + `MinRequestThreadCount` prewarms the pool to avoid thread-pool cold start. + + Set `DecoupleHeldPublishRequests = false` to restore the legacy behavior. + Held Publishes then occupy workers. A server handling N sessions needs a + pool well above N to prevent other services from returning + `BadRequestTimeout`. * `MaxFailedAuthenticationAttempts` (default 5; `0` disables) is the brute-force lockout threshold, keyed per client certificate. A single-certificate client that opens many sessions can trip it on transient handshake failures, after which further sessions are rejected with `BadUserAccessDenied`; disable or raise it for such clients. * Session establishment is CPU-bound (RSA handshakes) but parallelizes across cores; throttle/stagger concurrent connects and scale cores for bulk connection throughput. diff --git a/docs/CertificateManager.md b/docs/CertificateManager.md index 0f37d293c1..c20d271762 100644 --- a/docs/CertificateManager.md +++ b/docs/CertificateManager.md @@ -1,10 +1,48 @@ -## CertificateManager +# CertificateManager -The `CertificateManager` provides centralized certificate lifecycle management for OPC UA applications. It replaces the scattered certificate handling across `CertificateValidator`, `CertificateIdentifier`, `CertificateTypesProvider`, and `CertificateFactory` with a cohesive set of interfaces following the Interface Segregation Principle. +`CertificateManager` manages application certificates, trust lists, validation, +and lifecycle notifications. Use [Certificates](Certificates.md) for trust and +store concepts, this guide for current APIs, and the +[certificate migration guide](migrate/2.0.x/certificates.md) for 1.5.x API replacements. > **Note:** `CertificateIdentifier` is **metadata-only** (`StoreType` / `StorePath` / `SubjectName` / `Thumbprint` / `CertificateType` / `RawData` / `ValidationOptions`). It caches no `Certificate` and is not disposable. The `CertificateManager` (via `ICertificateRegistry`) is the single source of truth for materialized application certificates; `CertificateIdentifierResolver` is the stateless helper that materializes a `Certificate` from an identifier on demand. See *[Materializing a `Certificate` from a `CertificateIdentifier`](#materializing-a-certificate-from-a-certificateidentifier)* below. -### Architecture +## Contents + +- [Architecture](#architecture) +- [Quick Start](#quick-start) + - [Creating a CertificateManager](#creating-a-certificatemanager) + - [Instance-scoped store resolution](#instance-scoped-store-resolution) + - [Validating Certificates](#validating-certificates) + - [Subscribing to Certificate Changes](#subscribing-to-certificate-changes) + - [Updating Application Certificates](#updating-application-certificates) + - [Certificates for SecurityMode None](#certificates-for-securitymode-none) + - [Server-Side Certificate Rotation via Push (OPC UA Part 12 §7.10.9)](#server-side-certificate-rotation-via-push-opc-ua-part-12-7109) + - [TrustList-Change Effects on Channels, Sessions and Subscriptions (OPC UA Part 12 §7.10.9)](#trustlist-change-effects-on-channels-sessions-and-subscriptions-opc-ua-part-12-7109) + - [Origin-Independent Enforcement of Trust-Material Changes](#origin-independent-enforcement-of-trust-material-changes) + - [PushManagement Transactions (OPC UA Part 12 §7.10.2-§7.10.11)](#pushmanagement-transactions-opc-ua-part-12-7102-71011) + - [Certificate-Expiration and TrustList-Staleness Alarms (OPC UA Part 12 §7.8.3)](#certificate-expiration-and-trustlist-staleness-alarms-opc-ua-part-12-783) + - [Optional ServerConfiguration Surface (OPC UA Part 12 §7.10.3, §7.10.13, §7.10.20)](#optional-serverconfiguration-surface-opc-ua-part-12-7103-71013-71020) + - [TrustList Size Limits (OPC UA Part 12 §8.4.5)](#trustlist-size-limits-opc-ua-part-12-845) + - [Client-Side Auto-Detection of Certificate Changes](#client-side-auto-detection-of-certificate-changes) + - [Working with Trust-Lists](#working-with-trust-lists) +- [Interfaces Reference](#interfaces-reference) + - [ICertificateRegistry](#icertificateregistry) + - [ICertificateTrustListManager](#icertificatetrustlistmanager) + - [ICertificateValidatorEx](#icertificatevalidatorex) + - [ICertificateLifecycle](#icertificatelifecycle) + - [ITrustListFileAccess](#itrustlistfileaccess) + - [ICertificateFactory](#icertificatefactory) + - [ICertificateIssuer](#icertificateissuer) +- [Pluggable Store Backends](#pluggable-store-backends) +- [Certificate Wrapper and Reference Counting](#certificate-wrapper-and-reference-counting) +- [Backward Compatibility](#backward-compatibility) +- [OPC UA Specification Alignment](#opc-ua-specification-alignment) +- [Materializing a `Certificate` from a `CertificateIdentifier`](#materializing-a-certificate-from-a-certificateidentifier) + - [Resolving an identifier](#resolving-an-identifier) + - [When to register an in-memory certificate with the manager](#when-to-register-an-in-memory-certificate-with-the-manager) + +## Architecture The `CertificateManager` is composed of focused interfaces. Consumers depend only on the slice they need: @@ -21,9 +59,9 @@ Standalone (no CertificateManager dependency): └── ICertificateIssuer — "Sign as a CA" ``` -### Quick Start +## Quick Start -#### Creating a CertificateManager +### Creating a CertificateManager From an existing `SecurityConfiguration` (most common): @@ -52,7 +90,7 @@ await manager.LoadApplicationCertificatesAsync(securityConfiguration); The `CertificateManager` is also automatically initialized by `ServerBase` and `ApplicationInstance` during startup. -#### Instance-scoped store resolution +### Instance-scoped store resolution `CertificateManager` also implements the optional `ICertificateStoreResolver` capability. This interface is separate from `ICertificateManager`, so existing custom manager implementations @@ -83,7 +121,7 @@ GDS signing-request path and server-hosted `TrustList` access use it as well. Di can use the overload accepting `ICertificateStoreResolver`; existing constructors retain their behavior. Provider routing does not change trust acceptance, GDS access controls or transaction semantics. -#### Validating Certificates +### Validating Certificates ```csharp // Validate against the Peers trust-list (default) @@ -172,7 +210,7 @@ still used by active calls; new calls acquire the new configuration. Registry enumeration returns stable snapshots, and terminal manager disposal prevents late certificate publication. -#### Subscribing to Certificate Changes +### Subscribing to Certificate Changes ```csharp // Subscribe to all changes @@ -202,7 +240,7 @@ private class MyObserver : IObserver } ``` -#### Updating Application Certificates +### Updating Application Certificates ```csharp // Replace an application certificate (notifies all subscribers synchronously) @@ -218,7 +256,7 @@ take independent references before returning and release them after processing. The built-in client rotation pump does this for pending, replaced and processed notifications. -#### Certificates for SecurityMode None +### Certificates for SecurityMode None On `SecurityMode.None` endpoints, encrypted username or issued-token policies use an RSA application certificate compatible with every advertised encrypted @@ -235,7 +273,7 @@ configuration error; it never substitutes ECC or RSA_DH on a None endpoint. Anonymous, X.509 and explicitly unencrypted token policies retain their existing semantics. This follows [OPC 10000-4, 7.41](https://reference.opcfoundation.org/specs/OPC-10000-4/7.41.md). -#### Server-Side Certificate Rotation via Push (OPC UA Part 12 §7.10.9) +### Server-Side Certificate Rotation via Push (OPC UA Part 12 §7.10.9) When a client rotates a server's application certificate through the standard `ServerConfiguration.UpdateCertificate` + `ServerConfiguration.ApplyChanges` push flow, the server must — once the `ApplyChanges` response has been delivered — force the SecureChannels that were negotiated against the old certificate to renegotiate. The session (and any subscriptions) stay alive so the client's reconnect logic can transfer them onto a fresh channel. @@ -269,12 +307,12 @@ public sealed class MyTransportListener : ITransportListener, ITransportListener Server-base subclasses that want to observe rotation in custom ways can still subscribe to `ICertificateManager.CertificateChanges` and react to `ApplicationCertificateUpdated` events — but should not drive channel teardown from that hook (the event fires twice during a push update: once when the new cert is staged, once when `ApplyChanges` reloads, and only `ApplyChanges` has the real old-cert reference). -#### TrustList-Change Effects on Channels, Sessions and Subscriptions (OPC UA Part 12 §7.10.9) +### TrustList-Change Effects on Channels, Sessions and Subscriptions (OPC UA Part 12 §7.10.9) `ApplyChanges` also has to react to *TrustList* changes committed in the transaction, not just server-certificate rotation. Once the response has been delivered, `ConfigurationNodeManager` maps every committed TrustList to the certificate group it belongs to and applies the corresponding effect: - **Application group TrustList (`opc.tcp`)** validates the *peer* (client application) certificate presented in `OpenSecureChannel` against the `Peers` store. If a committed change (typically a removal of a trusted certificate or issuer) means a currently-connected peer would no longer validate, that peer's `opc.tcp` SecureChannel is forced to renegotiate; peers whose certificate is still trusted — and peers on a group whose TrustList did not change — keep their channels, Sessions and Subscriptions. -- **HTTPS group TrustList** validates the certificates used by the request-scoped UA-HTTPS transport (`Https` store). Because UA-HTTPS has no long-lived, per-peer SecureChannel keyed by the client application certificate, and any client TLS certificate (mutual TLS) is re-validated at the next TLS handshake by the shared validator against the directory-backed trust stores, an HTTPS-group change forces **no** channel renegotiation rather than a meaningless connection teardown. Server-*certificate* rotation on HTTPS is a separate concern handled by `ITransportListenerCertificateRotation` (a Kestrel Stop/Start cycle). +- **HTTPS group TrustList** validates certificates used by the request-scoped UA-HTTPS transport (`Https` store). UA-HTTPS has no long-lived, per-peer SecureChannel keyed by the client application certificate. The shared validator checks any client TLS certificate (mutual TLS) against the directory-backed trust stores at the next TLS handshake. Therefore, an HTTPS-group change does **not** trigger channel renegotiation or a connection teardown. Server-certificate rotation on HTTPS is separate; `ITransportListenerCertificateRotation` handles it with a Kestrel Stop/Start cycle. - **User-token group TrustList** validates X.509 user identity tokens. Every active Session that authenticated with a certificate user identity is re-validated against the updated user TrustList; a Session whose user certificate is no longer trusted is closed together with its Subscriptions (§7.10.9). Anonymous / username / issued-token Sessions are never disturbed. The effect fan-out runs after the same grace/flush boundary as certificate rotation and is awaitable through `IConfigurationNodeManager.DrainPendingApplyChangesAsync(CancellationToken)`. @@ -282,11 +320,12 @@ The effect fan-out runs after the same grace/flush boundary as certificate rotat Both TCP listeners retain the complete peer chain validated at `OpenSecureChannel`, including intermediates supplied only by the client. Their `ITransportListenerPeerCertificateChainRotation` capability passes an -owned chain snapshot to `CloseChannelsForUntrustedPeerChainsAsync`. The default -effect handler prefers this capability and validates without writing to the -rejected store. An unrelated trust addition therefore does not misclassify a -peer whose intermediate is absent from the server's issuer store; removal of -its trust anchor still closes the affected channel. +owned chain snapshot to `CloseChannelsForUntrustedPeerChainsAsync`. The +default effect handler prefers this capability and validates the chain +without writing to the rejected store. This prevents an unrelated trust +addition from misclassifying a peer whose intermediate is absent from the +server's issuer store. Removing the peer's trust anchor still closes its +channel. Custom transports should implement the chain-aware capability when peers can supply intermediates. The leaf-only capability below remains supported as a @@ -295,7 +334,22 @@ finishes, even if the channel closes concurrently. Two seams make this behaviour injectable and testable: -1. **`ITransportListenerPeerCertificateRotation`** — an optional capability interface on `ITransportListener` (sibling to `ITransportListenerCertificateRotation`). Each implementing listener advertises the single TrustList scope it validates its peer certificates against through `PeerCertificateTrustListScope`, and a committed change is routed to it **only** when the change targets that scope — so an `opc.tcp` listener is never re-validated or closed against the HTTPS store, and vice versa. Both `opc.tcp` listeners — the raw-socket `TcpTransportListener` and the Kestrel-hosted `KestrelTcpTransportListener` — implement it with the `Peers` scope, re-validating each tracked channel's client certificate through a caller-supplied async predicate and force-closing only the channels that fail (listener socket stays bound). The UA-HTTPS listener deliberately does **not** implement this capability (see the HTTPS bullet above). Custom transports opt in the same way: +1. **`ITransportListenerPeerCertificateRotation`** — an optional + `ITransportListener` capability, alongside + `ITransportListenerCertificateRotation`. Implementing listeners expose + their peer-certificate TrustList scope through + `PeerCertificateTrustListScope`. The server routes a committed change to + a listener only when the change targets that scope. This prevents an + `opc.tcp` listener from being revalidated or closed against the HTTPS + store, and vice versa. + + Both `opc.tcp` listeners implement this capability with the `Peers` scope: + the raw-socket `TcpTransportListener` and the Kestrel-hosted + `KestrelTcpTransportListener`. Each listener revalidates tracked client + certificates with a caller-supplied asynchronous predicate. It force-closes + only channels whose certificates fail validation, while keeping the + listener socket bound. The UA-HTTPS listener does **not** implement this + capability (see the HTTPS bullet above). Custom transports can opt in: ```csharp public sealed class MyTransportListener : ITransportListener, ITransportListenerPeerCertificateRotation @@ -316,11 +370,22 @@ Two seams make this behaviour injectable and testable: } ``` -2. **`IPushConfigurationTrustListEffectHandler`** — the injectable provider that performs the two effects above. `ConfigurationNodeManager` builds a `PushConfigurationTrustListEffectContext` (the committed effects plus the live transport listeners, session manager, certificate validator and a session-close delegate) and dispatches it to the handler after the grace boundary. A default `PushConfigurationTrustListEffectHandler` is created automatically; hosts can register their own through DI (`services.TryAddSingleton()`) or pass one to `MainNodeManagerFactory` / `ConfigurationNodeManager` directly. +2. **`IPushConfigurationTrustListEffectHandler`** — an injectable provider + that performs the two effects above. `ConfigurationNodeManager` creates a + `PushConfigurationTrustListEffectContext` containing the committed effects, + live transport listeners, session manager, certificate validator, and + session-close delegate. It dispatches the context to the handler after the + grace boundary. + + The stack creates a default `PushConfigurationTrustListEffectHandler`. + Hosts can register a custom handler through DI + (`services.TryAddSingleton()`) + or pass one directly to `MainNodeManagerFactory` or + `ConfigurationNodeManager`. The default handler's SecureChannel renegotiation summary logs only the aggregate number of affected channels, not their identifiers or certificate contents. -#### Origin-Independent Enforcement of Trust-Material Changes +### Origin-Independent Enforcement of Trust-Material Changes The effect fan-out above is not limited to changes committed through `ApplyChanges`. Two additional mechanisms guarantee that a trust-material change — most importantly a CRL that revokes a connected client's certificate — takes effect immediately instead of at the client's next security-token renewal (default lifetime: one hour): @@ -329,7 +394,7 @@ The effect fan-out above is not limited to changes committed through `ApplyChang Session semantics are unchanged: an application-group change (certificate or CRL) cuts only the affected SecureChannels — the Session survives for the client's reconnect logic, and dies with its timeout when the reconnect is rejected — while a user-token-group change closes the invalidated Sessions together with their Subscriptions. -#### PushManagement Transactions (OPC UA Part 12 §7.10.2-§7.10.11) +### PushManagement Transactions (OPC UA Part 12 §7.10.2-§7.10.11) `ConfigurationNodeManager` implements the full PushManagement transaction model: every `UpdateCertificate`, `CreateSelfSignedCertificate`, `DeleteCertificate` and TrustList (`AddCertificate` / `RemoveCertificate` / `Open`+`CloseAndUpdate`) call made within a Session is **staged**, not applied. Nothing takes effect until `ApplyChanges` is called; `CancelChanges` discards the staged work instead. This matches §7.10.2's *Transaction Lifecycle*: a transaction is created automatically on the first staging call, is owned exclusively by the Session that created it (every other Session's staging calls fail with `Bad_TransactionPending`), and is torn down by `ApplyChanges`, `CancelChanges`, or the owning Session closing. @@ -404,14 +469,40 @@ Both are also constructor parameters on `ConfigurationNodeManager`/`MainNodeMana Two further collaborators are internal to the stack and not replaceable: the `NamespaceMetadataRegistry` behind the Part 5 `NamespaceMetadata` members of `IConfigurationNodeManager`, and the `CertificateAlarmScheduler` that refreshes and evaluates the §7.8.3 alarms. `ConfigurationNodeManager` itself is a partial class split into one `ConfigurationNodeManager.*.cs` file per concern (Push methods, push validation, certificate slots, ApplyChanges, trust material, certificate alarms, namespace metadata). -**High-availability (distributed) transactions.** By default the PushManagement transaction is *per-server*: each replica of a `RedundantServerSet` would have its own independent transaction, so two administrators talking to two replicas could stage conflicting changes at once. Opting in with `UseDistributedPushConfiguration()` (from `Opc.Ua.Redundancy.Server`) replaces the two collaborators above with shared-store-backed variants so the transaction is single **across the whole replica set**: +**High-availability (distributed) transactions.** By default, each server +replica has its own PushManagement transaction. Two administrators using +different replicas could therefore stage conflicting changes. +`UseDistributedPushConfiguration()` (from `Opc.Ua.Redundancy.Server`) +replaces the two collaborators above with shared-store-backed variants. The +transaction is then shared **across the replica set**: -- `DistributedPushConfigurationTransactionCoordinator` wraps the per-server coordinator and guards it with a compare-and-swap lease in the shared `ISharedKeyValueStore` (the same lease pattern as `SharedStoreLeaseElection`). A replica may only start a transaction while it holds the lease, so at most one replica has an active transaction at a time; a replica that stops renewing (for example, because it crashed) loses the lease once it expires and a standby can take over. The lease is acquired at an `await` boundary immediately before the synchronous `Stage`, so the coordinator's synchronous `Stage`/`CancelChanges` contract is preserved with no sync-over-async. It is released as soon as the transaction is applied, cancelled, reset, or its owning Session closes. -- `SharedKeyValuePendingCertificateKeyStore` persists the regenerated `CreateSigningRequest` private key in the shared store instead of a local directory, so a `CreateSigningRequest` on one replica and the matching `UpdateCertificate` on another still complete. It is scoped per (CertificateGroup, CertificateType), consumes the key atomically (only one replica wins a concurrent `TryTake`), wipes every private-key working buffer, and — like the other distributed stores — protects each record with an `IRecordProtector` and fails closed if an external store is configured without one. +- `DistributedPushConfigurationTransactionCoordinator` wraps the per-server + coordinator and guards it with a compare-and-swap lease in the shared + `ISharedKeyValueStore`, following the `SharedStoreLeaseElection` pattern. + Only the replica holding the lease can start a transaction. If a replica + stops renewing the lease, for example after a crash, the lease expires and + a standby can take over. + + The coordinator acquires the lease at an `await` boundary immediately + before synchronous `Stage`. This preserves the synchronous + `Stage`/`CancelChanges` contract without sync-over-async. The coordinator + releases the lease when it applies, cancels, or resets the transaction, or + when the owning Session closes. +- `SharedKeyValuePendingCertificateKeyStore` stores regenerated + `CreateSigningRequest` private keys in the shared store rather than a local + directory. This lets one replica create a signing request and another + complete its matching `UpdateCertificate`. The store scopes keys by + `(CertificateGroup, CertificateType)` and consumes each key atomically, so + only one replica succeeds in a concurrent `TryTake`. It wipes private-key + working buffers and protects each record with an `IRecordProtector`. If an + external store lacks a protector, the store fails closed. This composes with the configured leader election. Setting `DistributedPushConfigurationOptions.RequireLeadership = true` additionally restricts transaction ownership to the elected leader; when the Kubernetes extension is configured (`UseKubernetesLeaderElection`), that leader is decided by the Kubernetes-native `Lease` already registered as the `ILeaderElection` service, reused without creating a second Kubernetes client. Distributed behaviour is opt-in — a server that does not call `UseDistributedPushConfiguration()` keeps the per-server coordinator and directory-backed pending-key store unchanged. See [High Availability](HighAvailability.md#distributed-pushmanagement-transactions-beyond-66-opt-in) for the full setup. -**Method security and validation.** The `ServerConfiguration` Methods enforce the per-Method SecureChannel and Role requirements from the §7.10 Method result tables. The **channel-security** requirement is checked first and reported separately from the **Role** requirement: +**Method security and validation.** `ServerConfiguration` methods enforce +the per-method SecureChannel and Role requirements in the §7.10 Method result +tables. The server checks and reports the **channel-security** requirement +separately, before it checks the **Role** requirement: - `UpdateCertificate` (§7.10.5) and `CreateSigningRequest` (§7.10.10) transfer private-key material and require an **encrypted** SecureChannel (`SignAndEncrypt`); a weaker channel is rejected with `Bad_SecurityModeInsufficient`. - `CreateSelfSignedCertificate` (§7.10.6), `DeleteCertificate` (§7.10.7), `GetCertificates` (§7.10.8), `GetRejectedList`, `ApplyChanges` (§7.10.9) and `CancelChanges` do not transfer a private key and accept any **authenticated** SecureChannel (`Sign` or `SignAndEncrypt`); an unauthenticated (`None`) channel is rejected with `Bad_SecurityModeInsufficient`. @@ -452,7 +543,7 @@ original commit failure with success. `DeleteCertificate`'s endpoint-reference safety check (§7.10.7: "Certificates that are referenced by EndpointDescriptions shall not be deleted. This determination happens when ApplyChanges is called.") resolves the exact certificate each active `EndpointDescription` presents **from the active certificate registry**, using its security policy and, on None endpoints, its encrypted user-token policies. It rejects the transaction with `Bad_InvalidState` at `ApplyChanges` if the deleted certificate is still referenced. Resolving from the live registry (rather than the `EndpointDescription.ServerCertificate` blob captured when the endpoints were created) ensures a certificate that was rotated after startup is still protected. A delete that is superseded within the same transaction by a `CreateSelfSignedCertificate`/`UpdateCertificate` for the same slot coalesces to the later operation (§7.10.2 ordered-queue semantics), so replacing a referenced certificate in one transaction remains allowed. A conservative net "last remaining certificate" check still runs at staging time for immediate feedback. -#### Certificate-Expiration and TrustList-Staleness Alarms (OPC UA Part 12 §7.8.3) +### Certificate-Expiration and TrustList-Staleness Alarms (OPC UA Part 12 §7.8.3) Certificate inputs come from an owned snapshot of the active certificate registry, including certificates originally loaded from stores. Group @@ -486,7 +577,7 @@ inactive and acknowledged. Alarm values are re-evaluated after every committed `UpdateCertificate`/`TrustList` change, so replacing an expiring certificate clears the alarm without waiting for the next tick. -#### Optional ServerConfiguration Surface (OPC UA Part 12 §7.10.3, §7.10.13, §7.10.20) +### Optional ServerConfiguration Surface (OPC UA Part 12 §7.10.3, §7.10.13, §7.10.20) Beyond the mandatory Push Certificate Management Methods, the `ServerConfigurationType` (§7.10.3) defines several **Optional** members. @@ -607,7 +698,7 @@ stream body (a serialized `UABinaryFileDataType` whose `SupportedDataType` is `ApplicationConfigurationDataType`) and is treated opaquely by the node manager, so the concrete encoding is entirely the provider's responsibility. -#### TrustList Size Limits (OPC UA Part 12 §8.4.5) +### TrustList Size Limits (OPC UA Part 12 §8.4.5) `ServerConfiguration.MaxTrustListSize` advertises, in bytes, the largest TrustList a Client may write (`0` = unlimited). Enforcing a truly unbounded @@ -618,7 +709,7 @@ hides a cap. The effective, actually-enforced limit is derived from the advertised `MaxTrustListSize` and `ServerConfigurationOptions.MaxTrustListSizeSafetyCeiling` -(default 1 MiB): +(default 1 MiB): | Advertised `MaxTrustListSize` | Effective (enforced **and** advertised) limit | | --- | --- | @@ -645,10 +736,10 @@ builder.ConfigureServerConfiguration(o => > The legacy `TrustList` constructor overloads that take only a single > `maxTrustListSize` (no explicit ceiling) are unchanged and fully backward > compatible: a finite size is honored exactly (never clamped) and `0` falls -> back to the 1 MiB default. The overload that also takes a +> back to the 1 MiB default. The overload that also takes a > `maxTrustListSizeSafetyCeiling` opts into the clamping semantics above. -#### Client-Side Auto-Detection of Certificate Changes +### Client-Side Auto-Detection of Certificate Changes `ManagedSession` automatically subscribes to `CertificateManager.CertificateChanges` and surfaces the three @@ -721,7 +812,7 @@ continue calling `Session.ReloadInstanceCertificateAsync` + their own reconnect logic. The `ApplicationCertificateChanged` event still fires either way. -#### Working with Trust-Lists +### Working with Trust-Lists ```csharp // Register a custom trust-list at runtime @@ -754,9 +845,9 @@ validation caches are invalidated in that case. > **Note:** `ITrustListTransaction`/`BeginUpdateAsync` above is a local, in-process staging API on `CertificateManager`. It is unrelated to the OPC UA PushManagement transaction model (`ApplyChanges`/`CancelChanges`, see *PushManagement Transactions* above), which is driven remotely by a Client over the `ServerConfiguration` address space and governs `TrustList`/`ServerConfiguration` Methods called through `ConfigurationNodeManager`. -### Interfaces Reference +## Interfaces Reference -#### ICertificateRegistry +### ICertificateRegistry Read-only access to the application's own certificates. @@ -776,7 +867,7 @@ Read-only access to the application's own certificates. > registry's own certificates, and the registry may concurrently replace its certificates > (e.g. a hot-update) without invalidating handles you already hold. -#### ICertificateTrustListManager +### ICertificateTrustListManager Manages an extensible set of named trust-lists. @@ -790,7 +881,7 @@ Manages an extensible set of named trust-lists. Well-known trust-lists: `TrustListIdentifier.Peers`, `.Users`, `.Https`, `.Rejected`. -#### ICertificateValidatorEx +### ICertificateValidatorEx Validates certificates against any trust-list. Works with both stored and ephemeral (wire-parsed) certificates. @@ -802,19 +893,16 @@ Validates certificates against any trust-list. Works with both stored and epheme Returns `CertificateValidationResult` with `IsValid`, `StatusCode`, `Errors`, and `IsSuppressible`. -##### Migrating from the legacy `CertificateValidation` event +#### Migrating from the legacy `CertificateValidation` event -Set the global `AcceptError` property once on `ApplicationConfiguration.CertificateValidator` (or any `ICertificateValidatorEx` instance) and a single delegate handles every validation: +Set the global `AcceptError` property on `ApplicationConfiguration.CertificateManager` +(or another `ICertificateValidatorEx` instance) after application-certificate initialization. +A per-call callback overrides the global callback for that validation. The example below +accepts untrusted certificates for development only; configure trust stores in production. ```csharp -// Before (legacy event): -config.CertificateValidator.CertificateValidation += (s, e) => -{ - if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted) { e.Accept = true; } -}; - -// After (modern global hook on ICertificateValidatorEx): -config.CertificateValidator.AcceptError = (cert, error) => +ICertificateManager manager = config.CertificateManager; +manager.AcceptError = (cert, error) => error.StatusCode == StatusCodes.BadCertificateUntrusted; // Or per-call (overrides the global hook for that call only): @@ -826,6 +914,9 @@ var options = new CertificateValidationOptions CertificateValidationResult result = await manager.ValidateAsync(chain, options: options); ``` +For the old-to-new event mapping, see +[certificate migration](migrate/2.0.x/certificates.md#migrating-the-certificatevalidatorcertificatevalidation-event). + Per-call validation behaviour can be overridden via `CertificateValidationOptions`: @@ -837,7 +928,7 @@ Per-call validation behaviour can be overridden via | `AutoAcceptUntrustedCertificates` | Override the global auto-accept policy. | | `AcceptError` | `Func` — invoked for each suppressible error encountered. Returning `true` accepts the specific error and validation continues. Structured replacement for the legacy `CertificateValidator.CertificateValidation += handler` + mutable `e.Accept = true` pattern. | -#### ICertificateLifecycle +### ICertificateLifecycle Monitors certificate changes and expiry. @@ -849,7 +940,7 @@ Monitors certificate changes and expiry. Change event kinds: `ApplicationCertificateUpdated`, `TrustListUpdated`, `CrlUpdated`, `CertificateRejected`, `CertificateExpiring`. -#### ITrustListFileAccess +### ITrustListFileAccess Read/write trust-lists as serialized blobs for GDS Push Management (Part 12 §7.5). @@ -858,7 +949,7 @@ Read/write trust-lists as serialized blobs for GDS Push Management (Part 12 §7. | `ReadTrustListAsync(TrustListIdentifier, TrustListMasks)` | Read trust-list contents | | `WriteTrustListAsync(TrustListIdentifier, TrustListData, TrustListMasks)` | Write trust-list contents | -#### ICertificateFactory +### ICertificateFactory Stateless certificate creation and parsing. Located in `Opc.Ua.Security.Certificates`. @@ -872,7 +963,7 @@ Stateless certificate creation and parsing. Located in `Opc.Ua.Security.Certific | `CreateWithPEMPrivateKey(...)` | Combine cert with PEM private key | | `CreateWithPrivateKey(...)` | Combine cert with private key from another cert | -#### ICertificateIssuer +### ICertificateIssuer CA signing and CRL revocation. Located in `Opc.Ua.Security.Certificates`. @@ -881,7 +972,7 @@ CA signing and CRL revocation. Located in `Opc.Ua.Security.Certificates`. | `IssueCertificate(ICertificateBuilder, Certificate)` | Sign a certificate with a CA key | | `RevokeCertificates(...)` | Produce an updated CRL | -### Pluggable Store Backends +## Pluggable Store Backends The rejected-certificate store limit uses zero for unlimited history and a negative value to disable new rejected-certificate storage. Negative limits @@ -929,7 +1020,7 @@ var manager = new CertificateManager( storeProviders: [new DirectoryStoreProvider(), new MyAzureKeyVaultProvider()]); ``` -### Certificate Wrapper and Reference Counting +## Certificate Wrapper and Reference Counting The `Certificate` class wraps `X509Certificate2` with reference counting: @@ -951,9 +1042,9 @@ ECC secret decryption clears owned plaintext, derived keys, IVs, and decoded temporary byte strings on success and failure, while preserving the returned secret and data outside the encrypted segment. -### Backward Compatibility +## Backward Compatibility -All migration is complete; the legacy `CertificateValidator` class, +The legacy `CertificateValidator` class, `ICertificateValidator` interface, `CertificateValidatorAdapter` bridge, `CertificateTypesProvider` class, and the legacy `ApplicationConfiguration.CertificateValidator` / @@ -976,7 +1067,7 @@ and forward to `Certificate.FromRawData(...)`, they remain the canonical default values used across configuration sites. -### OPC UA Specification Alignment +## OPC UA Specification Alignment | Spec Area | Interface | |-----------|-----------| @@ -995,8 +1086,7 @@ sites. The authoritative, per-requirement Part 12 support status — every implemented ServerConfiguration / PushManagement, TrustList, certificate-alarm, KeyCredentialService and AuthorizationService requirement linked to its source **and** its automated tests, with complete / partial / optional / unsupported marks — lives in the [GDS Conformance Matrix](GDS.md#conformance-matrix). That matrix also identifies the applicable OPC UA Facets / conformance units and states explicitly that the formal UACTT/CTT (a licensed GUI tool) is not run automatically here. - -### Materializing a `Certificate` from a `CertificateIdentifier` +## Materializing a `Certificate` from a `CertificateIdentifier` `CertificateIdentifier` is metadata only: `StoreType` / `StorePath` / `SubjectName` / `Thumbprint` / `CertificateType` / `RawData` / `ValidationOptions`. It caches no `Certificate`, is not disposable, and @@ -1005,7 +1095,7 @@ The authoritative, per-requirement Part 12 support status — every implemented returns `IList`, a public sealed record carrying `Certificate` plus `CertificateValidationOptions`. -#### Resolving an identifier +### Resolving an identifier Use the `CertificateIdentifierResolver` static helper: @@ -1045,7 +1135,7 @@ using ICertificateStore store = CertificateIdentifierResolver.OpenStore(id, tele The resolver always returns a caller-owned, `AddRef`'d `Certificate` (or `null`). The caller is responsible for disposing it. -#### When to register an in-memory certificate with the manager +### When to register an in-memory certificate with the manager If you have a `Certificate` instance that wasn't loaded from a configured store (for example, a freshly generated cert or one returned by a GDS push), persist it to a store and let the manager pick it up: diff --git a/docs/Certificates.md b/docs/Certificates.md index f49da2ea7c..d416b175b0 100644 --- a/docs/Certificates.md +++ b/docs/Certificates.md @@ -1,14 +1,73 @@ -## Certificates +# Certificates -All required application certificates for OPC UA are created at the first start of each application in a directory or OS-level certificate store and remain in use until deleted from the store. +The application creates its required OPC UA certificates in a directory or +OS-level certificate store on first startup. They remain in use until an +administrator deletes them from the store. -The UA stack allows also for using CA issued application certificates and remote certificate store and trust list management with a *Global Discovery Server* using *Server Push*. +The stack also supports application certificates issued by a certificate +authority (CA), plus remote certificate-store and trust-list management +through a *Global Discovery Server* using *Server Push*. -Private keys do not have to live in a file or an OS certificate store. A key can be held in a TPM, a smart card, an HSM or a remote key service and never enter process memory. See [CryptoProvider](CryptoProvider.md) for pluggable cryptography and hardware-held keys, and the `OPCFoundation.NetStandard.Opc.Ua.Security.Pkcs11` package for PKCS#11 tokens. +Private keys do not have to live in a file or OS certificate store. A TPM, +smart card, hardware security module (HSM), or remote key service can hold a +key without exposing it to process memory. See [CryptoProvider](CryptoProvider.md) +for pluggable cryptography and hardware-held keys, or use the +`OPCFoundation.NetStandard.Opc.Ua.Security.Pkcs11` package for PKCS#11 tokens. -### Certificate stores +## Contents -The layout of the certificate stores for sample applications which store the certificates in the file system follow the recommended layout in the [specification](https://reference.opcfoundation.org/v104/GDS/docs/F.1/), where certificates are stored in a `certs` folder, private keys under a `private` folder and revocation lists under a `crl` folder with a `` folder called `pki`. +- [Certificate stores](#certificate-stores) +- [X509Store on Windows](#x509store-on-windows) +- [Certificate and CertificateCollection Types](#certificate-and-certificatecollection-types) +- [Opening a certificate store](#opening-a-certificate-store) +- [Windows .NET applications](#windows-net-applications) +- [Windows UWP applications](#windows-uwp-applications) +- [.NET Core applications on Windows, Linux, iOS etc](#net-core-applications-on-windows-linux-ios-etc) +- [Certificate Validation](#certificate-validation) + - [Validation Workflow](#validation-workflow) + - [Chain Building Process](#chain-building-process) + - [Overview](#overview) + - [Chain Building Algorithm Diagram](#chain-building-algorithm-diagram) + - [Detailed Algorithm Steps](#detailed-algorithm-steps) + - [Step 1: Initialization](#step-1-initialization) + - [Step 2: Iterative Chain Building Loop](#step-2-iterative-chain-building-loop) + - [Step 3: Issuer Matching Algorithm](#step-3-issuer-matching-algorithm) + - [Step 4: Certificate Revocation List (CRL) Checking](#step-4-certificate-revocation-list-crl-checking) + - [X509Chain Validation](#x509chain-validation) + - [Chain Validation Results](#chain-validation-results) + - [Key Behaviors](#key-behaviors) + - [Certificate List Configuration](#certificate-list-configuration) + - [Configuration Sources](#configuration-sources) + - [Configuration File Structure](#configuration-file-structure) + - [Certificate Store Types](#certificate-store-types) + - [Certificate List Population](#certificate-list-population) + - [1. Initialization via ApplicationConfiguration](#1-initialization-via-applicationconfiguration) + - [2. Internal Update Process](#2-internal-update-process) + - [3. Certificate Search Behavior](#3-certificate-search-behavior) + - [Runtime Certificate Management](#runtime-certificate-management) + - [Certificate Store Management](#certificate-store-management) + - [Dual-Mode Operation](#dual-mode-operation) + - [Configuration Best Practices](#configuration-best-practices) + - [Configuration Settings](#configuration-settings) + - [AutoAcceptUntrustedCertificates](#autoacceptuntrustedcertificates) + - [RejectSHA1SignedCertificates](#rejectsha1signedcertificates) + - [RejectUnknownRevocationStatus](#rejectunknownrevocationstatus) + - [MinimumCertificateKeySize](#minimumcertificatekeysize) + - [UseValidatedCertificates](#usevalidatedcertificates) + - [MaxRejectedCertificates](#maxrejectedcertificates) + - [Suppressible Validation Errors](#suppressible-validation-errors) + - [CA (issuer) KeyUsage validation](#ca-issuer-keyusage-validation) + - [Inspecting Certificate Validation Results](#inspecting-certificate-validation-results) + - [Configuring a Custom Certificate Validator](#configuring-a-custom-certificate-validator) + - [Best Practices](#best-practices) + +## Certificate stores + +File-based sample applications follow the certificate-store layout +recommended in the [specification](https://reference.opcfoundation.org/v104/GDS/docs/F.1/). +The root folder, called `pki`, contains a `certs` folder for certificates, a +`private` folder for private keys, and a `crl` folder for certificate +revocation lists (CRLs). The UA .NET Standard stack supports the following certificate stores: @@ -18,7 +77,11 @@ The UA .NET Standard stack supports the following certificate stores: - The **Trusted** store `/trusted`which contains certificates which are trusted by the application. The certificates in this store can either be self signed, leaf, root CA or sub CA certificates. The most common use case is to add a self signed application certificate to the *Trusted* store to establish trust with that application. - If the application certificate is the leaf of a chain, the trust can be established by adding the root CA, a sub CA or the leaf certificate itself to the *Trusted* store. Each of the options enables a different set of trusted certificates. A trusted Root CA or Sub CA certificate is used as the trust anchor for the certificate chain, which means any leaf certificate with a chain which contains the Root CA and Sub CA certificate is trusted, but the specification still mandates the validation of the whole chain. For the chain validation any certificate in the chain except the leaf certificate must be available from the *Issuer* store. + If the application certificate is the leaf of a chain, the trust can be established by adding the root CA, a sub CA or the leaf certificate itself to the *Trusted* store. Each of the options enables a different set of trusted certificates. A trusted Root CA or Sub CA certificate acts as the chain's trust anchor. + The application trusts a leaf certificate when its chain contains that + anchor. The specification still requires validation of the entire chain. + For this validation, the *Issuer* store must contain every certificate in + the chain except the leaf. If only the leaf certificate is in the *Trusted* store and the rest of the chain is stored in the *Issuer* store, then only the leaf certificate is trusted. As an example, to trust an application certificate that is issued by a Root CA, only the Root CA certificate is required in the *Trusted* store to establish trust to all application certificates issued by the CA. This option can greatly simplify the management of OPC UA Clients and Servers because only one certificate needs to be distributed across all systems. @@ -33,12 +96,12 @@ The UA .NET Standard stack supports the following certificate stores: - The **Trusted Https** store `/trustedHttps` which contains https certificates which are trusted by an application. To establish trust, the same rules apply as explained for the *Trusted* and the *Issuer* store. -### X509Store on Windows +## X509Store on Windows On the **Windows OS** the X509Store supports the storage and retrieval of CRLs. This enables the usage of the X509Store instead of the Directory Store for stores requiring the use of crls, e.g. the issuer or the directory Store. -### Certificate and CertificateCollection Types +## Certificate and CertificateCollection Types The stack uses the `Certificate` wrapper type (in `Opc.Ua.Security.Certificates`) instead of `X509Certificate2` directly. `Certificate` wraps `X509Certificate2` with reference counting for safe shared ownership — the inner `X509Certificate2` is disposed only when the last reference is released. Use `Certificate.AddRef()` before sharing and `Dispose()` to release. @@ -48,7 +111,7 @@ For interop with .NET APIs that require `X509Certificate2`, use `certificate.AsX See [CertificateManager.md](CertificateManager.md#certificate-wrapper-and-reference-counting) for details. -### Opening a certificate store +## Opening a certificate store A `CertificateStoreIdentifier` is only a *description* of a store (`StoreType` / `StorePath`) — the analogue of a `CertificateIdentifier` resolving to a `Certificate`. Every call to `OpenStore` creates and opens a new `ICertificateStore` instance that the caller owns and must dispose: @@ -58,19 +121,22 @@ using ICertificateStore store = identifier.OpenStore(telemetry); CertificateCollection certificates = await store.EnumerateAsync(ct); ``` -A component that accesses a store repeatedly should open one instance, keep it for the component's lifetime and dispose it at shutdown — the store refreshes its parsed-certificate cache itself when the backing data changes — rather than re-open the store per operation. +A component that accesses a store repeatedly should open one instance and +keep it for the component's lifetime. Dispose the store at shutdown instead +of reopening it for each operation. The store refreshes its parsed-certificate +cache when the backing data changes. -### Windows .NET applications +## Windows .NET applications By default the self signed certificates are stored in a **X509Store** called **CurrentUser\\UA_MachineDefault**. The certificates can be viewed or deleted with the Windows Certificate Management Console (certmgr.msc). The *trusted*, *issuer* and *rejected* stores remain in a folder called **OPC Foundation\pki** with a root folder which is specified by the `SpecialFolder` variable **%CommonApplicationData%**. On Windows 7/8/8.1/10 this is usually the invisible folder **C:\ProgramData**. -### Windows UWP applications +## Windows UWP applications By default the self signed certificates are stored in a **X509Store** called **CurrentUser\\UA_MachineDefault**. The certificates can be viewed or deleted with the Windows Certificate Management Console (certmgr.msc). The *trusted*, *issuer* and *rejected* stores remain in a folder called **OPC Foundation\pki** in the **LocalState** folder of the installed universal windows package. Deleting the application state also deletes the certificate stores. -### .NET Core applications on Windows, Linux, iOS etc +## .NET Core applications on Windows, Linux, iOS etc The self signed certificates are stored in a folder called **OPC Foundation/pki/own** with a root folder which is specified by the `SpecialFolder` variable **%LocalApplicationData%** or in a **X509Store** called **CurrentUser\\My**, depending on the configuration. For best cross platform support the personal store **CurrentUser\\My** was chosen to support all platforms with the same configuration. Some platforms, like macOS, do not support arbitrary certificate stores. @@ -78,7 +144,13 @@ The *trusted*, *issuer* and *rejected* stores remain in a shared folder called * ## Certificate Validation -The OPC UA .NET Standard Stack validates certificates according to the OPC UA specification. The new `CertificateManager` provides centralized certificate management with trust-list-scoped validation, lifecycle monitoring, and pluggable store backends (see [CertificateManager.md](CertificateManager.md)). The legacy `CertificateValidator` class remains supported via a backward compatibility adapter. This section describes the certificate validation workflow, configuration settings, and how to customize the validation process. +The OPC UA .NET Standard Stack validates certificates according to the OPC UA +specification. `CertificateManager` provides centralized certificate +management with trust-list-scoped validation, lifecycle monitoring, and +pluggable store backends (see [CertificateManager.md](CertificateManager.md)). +The legacy validator and adapter are removed. This section explains trust, +validation workflow, and configuration; the current API reference lives in +[CertificateManager](CertificateManager.md#icertificatevalidatorex). ### Validation Workflow @@ -86,7 +158,10 @@ The certificate validation process follows these steps: 1. **Structural Check**: Any certificate in the chain whose **subject or issuer is an empty distinguished name** is rejected immediately with the non-suppressible `Bad_CertificateInvalid`. An empty name is an empty `RDNSequence`: it identifies nothing, and two unrelated issuers become indistinguishable, so such a certificate can never take part in a trust decision. RFC 5280 §4.1.2.4 requires a non-empty issuer, and §4.1.2.6 only permits an empty subject for an end entity carrying a critical `subjectAltName` — which a CA may never do. This check runs before the trust-list lookup and the chain build so the outcome is identical on every target framework, rather than depending on whether the platform's X.509 parser happens to accept the certificate. -2. **Pre-validation Check**: If the certificate was previously validated and `UseValidatedCertificates` is enabled, the validation is skipped. +2. **Validation Cache**: `UseValidatedCertificates` can skip expensive chain-policy + rebuilding for a previously accepted certificate. The issuer walk and revocation + checks still run before a cached result is used, so a newly revoked certificate + is not accepted solely because it was previously cached. 3. **Trust Check**: The validator checks if the certificate is explicitly trusted by searching in: - The **trusted certificate list** (`TrustedPeerCertificates.TrustedCertificates`) - An in-memory collection of explicitly trusted certificates @@ -109,7 +184,7 @@ The certificate validation process follows these steps: 7. **Application URI Validation**: Verifies that the certificate contains the expected Application URI in the Subject Alternative Name extension. 8. **Error Handling**: If validation errors occur, they are classified as either: - - **Suppressible errors**: Can be accepted via the `CertificateValidation` event callback + - **Suppressible errors**: Can be accepted via `CertificateValidationOptions.AcceptError` or the manager's global `AcceptError` callback - **Non-suppressible errors**: Always cause validation to fail 9. **Rejected Certificate Storage**: Failed certificates are saved to the rejected certificate store for administrator review. @@ -118,7 +193,10 @@ Certificates with an empty distinguished name are also skipped when a PEM file i ### Chain Building Process -This section provides detailed technical documentation of the certificate chain building and validation algorithm implemented in `CertificateValidator.GetIssuersNoExceptionsOnGetIssuerAsync()`. +This section explains the chain-building and validation algorithm used by +`CertificateManager`. The implementation is in the internal +[`CertificateValidationCore`](../src/Opc.Ua.Core/Security/Certificates/CertificateManager/CertificateValidationCore.cs); +applications use `ICertificateValidatorEx`, not that internal class. #### Overview @@ -461,7 +539,8 @@ Certificate lists are populated from two primary sources: #### Configuration File Structure -The `SecurityConfiguration` section in the application configuration file (`*.Config.xml`) defines certificate stores: +The application configuration file (`*.Config.xml`) defines certificate +stores in its `SecurityConfiguration` section: ```xml @@ -536,50 +615,34 @@ unverifiable or overly broad permissions are never silently accepted. #### Certificate List Population -Both the new `CertificateManager` and the legacy `CertificateValidator` -(via the `CertificateValidatorAdapter` bridge) source their trust lists -from the same `SecurityConfiguration` defined in -`ApplicationConfiguration`. +`CertificateManager` sources its well-known trust lists from the +`SecurityConfiguration` in `ApplicationConfiguration`. Hosts may also register +named trust lists for other purposes. ##### 1. Initialization via ApplicationConfiguration ```csharp -// Load configuration from file -ApplicationConfiguration config = await ApplicationConfiguration - .Load(new FileInfo("MyApp.Config.xml"), ApplicationType.Client, null) - .ConfigureAwait(false); - -// Recommended: build a CertificateManager from the SecurityConfiguration. -// CertificateManagerFactory automatically registers the well-known trust -// lists (Peers, Users, Https, Rejected) from the configuration. +// Direct-construction alternative for an already loaded configuration. using CertificateManager manager = CertificateManagerFactory.Create( config.SecurityConfiguration, telemetry); await manager .LoadApplicationCertificatesAsync(config.SecurityConfiguration, config.ApplicationUri) .ConfigureAwait(false); - -// `ApplicationInstance` and `ServerBase` automatically construct and own -// the `CertificateManager` during `Start*Async`. Access it via -// `applicationInstance.CertificateManager` or `Server.CertificateManager`. ``` +For a hosted application, use `config.CertificateManager` after application +certificate initialization instead of creating another manager. See +[initialization ordering](migrate/2.0.x/certificates.md#removed-certificate-apis). + ##### 2. Internal Update Process -When `UpdateAsync()` is called, the validator performs these steps: - -``` -// From SecurityConfiguration -trustedStore = config.SecurityConfiguration.TrustedPeerCertificates -issuerStore = config.SecurityConfiguration.TrustedIssuerCertificates - -// Populate internal structures -m_trustedCertificateStore = trustedStore.StorePath -m_trustedCertificateList = trustedStore.TrustedCertificates (if specified) -m_issuerCertificateStore = issuerStore.StorePath -m_issuerCertificateList = issuerStore.TrustedCertificates (if specified) -m_applicationCertificates = config.SecurityConfiguration.ApplicationCertificates -``` +The factory registers the configured trusted and issuer stores. Validation uses +the selected trust list's certificate lists and stores; application code does +not populate private validator fields or call a legacy `UpdateAsync` method. +For changing a live trust list, use +[trust-list transactions](CertificateManager.md#working-with-trust-lists). +The manager coordinates change notifications and validation-cache invalidation. ##### 3. Certificate Search Behavior @@ -587,14 +650,14 @@ During validation, certificates are searched in the following order: **For Trusted Certificates:** -1. Search `m_trustedCertificateList` (explicit list) - if populated -2. Search `m_trustedCertificateStore` (file system or X509Store) -3. Search `m_applicationCertificates` (application's own certificates) +1. Search the configured explicit trusted-certificate list, if populated. +2. Search the trusted store (file system or X509Store). +3. Check the application's own certificates. **For Issuer Certificates:** -1. Search `m_issuerCertificateList` (explicit list) - if populated -2. Search `m_issuerCertificateStore` (file system or X509Store) +1. Search the configured explicit issuer-certificate list, if populated. +2. Search the issuer store (file system or X509Store). #### Runtime Certificate Management @@ -752,7 +815,18 @@ All other validation errors are **non-suppressible** and will always cause the v #### CA (issuer) KeyUsage validation -OPC UA requires every CA (issuer) certificate in a chain to carry a KeyUsage extension that asserts both `keyCertSign` and `cRLSign` (OPC 10000-6 §6.2.4, Table 52 — Issuer Certificate). During chain validation the stack verifies this for each issuer certificate and reports the suppressible `BadCertificateIssuerUseNotAllowed` error (OPC 10000-4 §6.1.3, Table 100 — "Certificate Usage") when a CA certificate is missing these bits, including the case where the CA has no KeyUsage extension at all. This matches the behaviour of strict third-party OPC UA stacks, which reject such CA certificates (typically with `BadCertificateInvalid`). CA certificates created by this stack's `CertificateBuilder` always include the required bits; to interoperate with a legacy CA that does not, suppress the error as described above. +OPC UA requires every CA (issuer) certificate in a chain to carry a KeyUsage +extension with both `keyCertSign` and `cRLSign` set (OPC 10000-6 §6.2.4, +Table 52 — Issuer Certificate). During chain validation, the stack checks +each issuer certificate. If a CA certificate lacks either bit or has no +KeyUsage extension, the stack reports the suppressible +`BadCertificateIssuerUseNotAllowed` error (OPC 10000-4 §6.1.3, Table 100, +"Certificate Usage"). + +Strict third-party OPC UA stacks also reject such CA certificates, typically +with `BadCertificateInvalid`. This stack's `CertificateBuilder` includes the +required bits in every CA certificate it creates. To interoperate with a +legacy CA that omits them, suppress the error as described above. ### Inspecting Certificate Validation Results @@ -811,77 +885,26 @@ as `ApplicationCertificateUpdated`, `TrustListUpdated`, `CrlUpdated`, `CertificateRejected`, and `CertificateExpiring`. See [CertificateManager.md](CertificateManager.md) for the full reference. -> **Legacy callback (deprecated)**: the -> `CertificateValidator.CertificateValidation` event with mutable -> `e.Accept = true` continues to work for existing applications via the -> backward‑compat `CertificateValidator` class. New code should prefer -> the structured result above. +The legacy `CertificateValidator.CertificateValidation` event and adapter are +removed. See [certificate migration](migrate/2.0.x/certificates.md#migrating-the-certificatevalidatorcertificatevalidation-event) +for the old-to-new mapping. ### Configuring a Custom Certificate Validator -To use a custom certificate validator, implement the new -`ICertificateValidatorEx` interface (or wrap your implementation in a -`CertificateValidatorAdapter` to expose the legacy -`ICertificateValidator` surface): +Use `ApplicationConfiguration.CertificateManager` for the application's normal +validation path. It combines trust-list management, certificate validation, and +certificate lifecycle notifications. Keep those checks in place when adding +application-specific requirements; an always-successful validator bypasses trust, +validity, and revocation checks. -```csharp -public sealed class CustomCertificateValidator : ICertificateValidatorEx -{ - public Task ValidateAsync( - Certificate certificate, - TrustListIdentifier? trustList = null, - CancellationToken ct = default) - { - return ValidateAsync( - new CertificateCollection(new[] { certificate }), - trustList, - options: null, - ct); - } - - public Task ValidateAsync( - CertificateCollection chain, - TrustListIdentifier? trustList = null, - CertificateValidationOptions? options = null, - CancellationToken ct = default) - { - Certificate certificate = chain[0]; - - if (!MeetsCustomRequirements(certificate)) - { - return Task.FromResult(new CertificateValidationResult( - isValid: false, - statusCode: StatusCodes.BadCertificateInvalid, - errors: new[] - { - new ServiceResult( - StatusCodes.BadCertificateInvalid, - "Certificate does not meet custom requirements.") - }, - isSuppressible: false)); - } - - return Task.FromResult(CertificateValidationResult.Success); - } - - private static bool MeetsCustomRequirements(Certificate certificate) - { - // Implement your custom validation logic - return true; - } -} - -// Bridge a custom ICertificateValidatorEx to legacy ICertificateValidator: -ICertificateValidator legacyApi = new CertificateValidatorAdapter( - new CustomCertificateValidator()); -``` - -> **Note**: Replacing -> `ApplicationConfiguration.CertificateValidator` with a custom -> implementation is still supported for backward compatibility but is -> discouraged in new code. Prefer providing a custom -> `ICertificateValidatorEx` (or `ICertificateManager`) and consuming it -> directly in your transport / channel pipeline. +The current validation contract is `ICertificateValidatorEx`. Its +`CertificateValidationOptions.AcceptError` callback can accept individual +suppressible errors; accepting untrusted certificates is appropriate only for +controlled development environments. For the interface, initialization order, +callback precedence, and examples, see +[CertificateManager validation](CertificateManager.md#icertificatevalidatorex). +There is no `CertificateValidatorAdapter` or legacy +`ApplicationConfiguration.CertificateValidator` property in the current API. ### Best Practices diff --git a/docs/ComplexTypes.md b/docs/ComplexTypes.md index 83f6500f45..5a614d76c5 100644 --- a/docs/ComplexTypes.md +++ b/docs/ComplexTypes.md @@ -2,18 +2,89 @@ ## Overview -The `Opc.Ua.Client` and `Opc.Ua.Client.ComplexTypes` libraries provide support for handling custom data types (complex types) in OPC UA client applications. Complex types include: +The `Opc.Ua.Client` and `Opc.Ua.Client.ComplexTypes` libraries let client +applications handle custom data types (complex types) in Open Platform +Communications Unified Architecture (OPC UA). Complex types include: - **Custom Structures**: User-defined structured data types with multiple fields - **Custom Enumerations**: User-defined enumeration types with custom values -The library allows OPC UA clients to automatically discover, load, and work with server-specific custom types, enabling seamless reading and writing of structured data without manual type definitions. +The libraries let clients discover and load server-specific types. Clients +can then read and write structured values without manually defining each type. + +## Contents + +- [Overview](#overview) +- [Key Concepts](#key-concepts) + - [What are Complex Types?](#what-are-complex-types) + - [Type Discovery and Loading](#type-discovery-and-loading) + - [Supported Type Systems](#supported-type-systems) +- [Getting Started](#getting-started) + - [Type builders](#type-builders) + - [Default type builder](#default-type-builder) + - [Reflection.Emit based type builder](#reflectionemit-based-type-builder) + - [Basic Usage](#basic-usage) + - [1. Loading All Custom Types from a Server](#1-loading-all-custom-types-from-a-server) + - [2. Reading Values with Complex Types](#2-reading-values-with-complex-types) + - [3. Writing Values with Complex Types](#3-writing-values-with-complex-types) +- [Approaches for Working with Custom Types](#approaches-for-working-with-custom-types) + - [Approach 1: Hand-Written IEncodeable + EncodeableFactory Registration](#approach-1-hand-written-iencodeable--encodeablefactory-registration) + - [Step 1 – Implement IEncodeable](#step-1--implement-iencodeable) + - [Step 2 – Register the Type Before Reading](#step-2--register-the-type-before-reading) + - [Approach 2: Source-Generated IEncodeable (Recommended for New Code)](#approach-2-source-generated-iencodeable-recommended-for-new-code) + - [Approach 3: Runtime IStructure (No Pre-defined Types Required)](#approach-3-runtime-istructure-no-pre-defined-types-required) +- [Advanced Usage](#advanced-usage) + - [Loading Specific Types](#loading-specific-types) + - [Load a Specific Type](#load-a-specific-type) + - [Load a Specific Type with Subtypes](#load-a-specific-type-with-subtypes) + - [Load All Types from a Namespace](#load-all-types-from-a-namespace) + - [Working with Complex Type Properties](#working-with-complex-type-properties) + - [Access by Name](#access-by-name) + - [Access by Index](#access-by-index) + - [Enumerate Properties](#enumerate-properties) + - [Handling Enumeration Types](#handling-enumeration-types) + - [Type Information and Introspection](#type-information-and-introspection) + - [Get All Loaded Types](#get-all-loaded-types) + - [Get Type Definitions](#get-type-definitions) + - [Get Loaded Data Type IDs](#get-loaded-data-type-ids) + - [Using Custom Type Factories](#using-custom-type-factories) + - [Working with Telemetry and Logging](#working-with-telemetry-and-logging) +- [Common Patterns](#common-patterns) + - [Pattern 1: One-Time Type Loading at Session Start](#pattern-1-one-time-type-loading-at-session-start) + - [Pattern 2: Lazy Loading on Demand](#pattern-2-lazy-loading-on-demand) + - [Pattern 3: Reading Multiple Complex Values](#pattern-3-reading-multiple-complex-values) +- [Error Handling](#error-handling) + - [Handling Type Loading Failures](#handling-type-loading-failures) + - [Handling Missing Type Definitions](#handling-missing-type-definitions) +- [Performance Considerations](#performance-considerations) + - [Type System Caching](#type-system-caching) + - [Minimizing Load Time](#minimizing-load-time) + - [Batch Operations](#batch-operations) +- [Troubleshooting](#troubleshooting) + - [Types Not Loading](#types-not-loading) + - [Values Still Encoded as ExtensionObject](#values-still-encoded-as-extensionobject) + - [Performance Issues](#performance-issues) +- [Server-Side Complex Types](#server-side-complex-types) + - [How compiled types reach the factory](#how-compiled-types-reach-the-factory) + - [Configuring via dependency injection (recommended)](#configuring-via-dependency-injection-recommended) + - [Direct usage without dependency injection](#direct-usage-without-dependency-injection) +- [API Reference](#api-reference) + - [ComplexTypeSystem Class](#complextypesystem-class) + - [Constructors](#constructors) + - [Methods](#methods) + - [Properties](#properties) + - [IStructure Interface](#istructure-interface) + - [IStructureField Interface](#istructurefield-interface) +- [Known Limitations](#known-limitations) +- [Additional Resources](#additional-resources) +- [See Also](#see-also) ## Key Concepts ### What are Complex Types? -In OPC UA, complex types are custom data types defined by the server that extend beyond the built-in OPC UA data types. These types are commonly used to represent structured data such as: +In OPC UA, complex types are custom data types that a server defines beyond +the built-in data types. They commonly represent structured data such as: - Configuration structures with multiple parameters - Device status information with multiple fields @@ -21,11 +92,12 @@ In OPC UA, complex types are custom data types defined by the server that extend ### Type Discovery and Loading -The `ComplexTypeSystem` class manages the discovery and loading of custom types from an OPC UA server. It: +The `ComplexTypeSystem` class discovers and loads custom types from an OPC UA +server. It: 1. Browses the server's type system to discover custom types 2. Loads type definitions (using DataTypeDefinition attribute or binary/XML dictionaries) -3. Registers types complying to the type definitions in the session's type factory for encoding/decoding +3. Registers types that match those definitions in the session's type factory for encoding and decoding. ### Supported Type Systems @@ -75,30 +147,20 @@ using Opc.Ua.Client.ComplexTypes; // client Create(...) helpers + NodeCacheResol // Create and connect session var session = await Session.Create(...); -// Create and load the complex type system -ComplexTypeSystem complexTypeSystem; -if (!useReflectionEmitTypeBuilder) -{ - // Uses the default (NativeAOT friendly) type builder - complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); -} -else -{ - // Uses the Reflection.Emit type builder. Only works if the - // OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes nuget is referenced. - complexTypeSystem = ComplexTypeSystem.Create( - session, new ComplexTypeBuilderFactory(), session.MessageContext.Telemetry); -} - +// Uses the default NativeAOT-friendly builder. +using var complexTypeSystem = ComplexTypeSystem.Create( + session, session.MessageContext.Telemetry); await complexTypeSystem.LoadAsync(); Console.WriteLine($"Loaded {complexTypeSystem.GetDefinedTypes().Count} custom types"); - -// The type system owns the node cache it resolves through; dispose it once no -// more types are loaded. Types already loaded stay registered with the session. -complexTypeSystem.Dispose(); ``` +The type system owns its resolver; the `using` declaration releases it even when +loading fails. Types already loaded remain registered with the session. The +optional Reflection.Emit builder is selected through the +[custom factory overload](#using-custom-type-factories), using +`new ComplexTypeBuilderFactory()` from the separate Client.ComplexTypes package. + After loading, the session can automatically encode and decode custom types when reading or writing values. #### 2. Reading Values with Complex Types @@ -356,7 +418,7 @@ Instead of loading all types, you can load specific types or namespaces: #### Load a Specific Type ```csharp -var complexTypeSystem = new ComplexTypeSystem(session); +using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); // Load a specific type by NodeId ExpandedNodeId typeNodeId = new ExpandedNodeId("ns=2;i=3001"); @@ -379,7 +441,7 @@ IType? systemType = await complexTypeSystem.LoadTypeAsync(typeNodeId, subTypes: #### Load All Types from a Namespace ```csharp -var complexTypeSystem = new ComplexTypeSystem(session); +using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); // Load all custom types from a specific namespace string namespaceUri = "http://mycompany.com/MyCustomTypes"; @@ -464,7 +526,7 @@ if (dataValue.WrappedValue.TryGetValue(out EnumValue enumValue)) #### Get All Loaded Types ```csharp -var complexTypeSystem = new ComplexTypeSystem(session); +using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); await complexTypeSystem.LoadAsync(); // Get all types that were dynamically created @@ -511,85 +573,69 @@ You can provide a custom type factory for advanced scenarios: IComplexTypeFactory customFactory = new MyCustomComplexTypeFactory(); // Use it with the ComplexTypeSystem -var complexTypeSystem = new ComplexTypeSystem(session, customFactory); +using var complexTypeSystem = ComplexTypeSystem.Create( + session, customFactory, session.MessageContext.Telemetry); await complexTypeSystem.LoadAsync(); ``` ### Working with Telemetry and Logging -The ComplexTypeSystem supports the OPC UA telemetry context for observability: +Pass the session's telemetry context, or an application-provided `ITelemetryContext`, +to the factory. The type system logs loading diagnostics through that context. ```csharp -// Use the session's telemetry context (default) -var complexTypeSystem = new ComplexTypeSystem(session); - -// Or provide a custom telemetry context -ITelemetryContext telemetry = myCustomTelemetryContext; -var complexTypeSystem = new ComplexTypeSystem(session, telemetry); - -// The system will log type loading information +ITelemetryContext telemetry = session.MessageContext.Telemetry; +using var complexTypeSystem = ComplexTypeSystem.Create(session, telemetry); await complexTypeSystem.LoadAsync(); ``` +Keep the type system alive while loading types, and dispose it afterward even if +loading fails. Loaded types remain registered with the session's factory. + ## Common Patterns ### Pattern 1: One-Time Type Loading at Session Start +After establishing a session with the [managed client API](Sessions.md), load +its types before reading custom values: + ```csharp -public async Task CreateSessionWithTypes(string endpointUrl) -{ - var session = await Session.Create( - configuration, - new ConfiguredEndpoint(null, new EndpointDescription(endpointUrl)), - false, - "MyClient", - 60000, - null, - null - ); - - // Load all custom types immediately after connection - var complexTypeSystem = new ComplexTypeSystem(session); - await complexTypeSystem.LoadAsync(); - - return session; -} +await using ManagedSession session = await new ManagedSessionBuilder(configuration, telemetry) + .UseEndpoint(endpoint) + .ConnectAsync(ct); +using var complexTypeSystem = ComplexTypeSystem.Create(session, telemetry); +await complexTypeSystem.LoadAsync(ct: ct); ``` +Keep the session alive for subsequent reads. Disposing the loader releases its +resolver, not the types registered in the session. + ### Pattern 2: Lazy Loading on Demand +Load a known DataType only when needed, then read its value. This avoids keeping +a loader field alive merely to retain registered types: + ```csharp -public class OpcUaClient +using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); +IType? loadedType = await complexTypeSystem.LoadTypeAsync(typeNodeId); +if (loadedType == null) { - private ISession _session; - private ComplexTypeSystem _complexTypeSystem; - private bool _typesLoaded; - - public async Task EnsureTypesLoadedAsync() - { - if (!_typesLoaded) - { - _complexTypeSystem = new ComplexTypeSystem(_session); - await _complexTypeSystem.LoadAsync(); - _typesLoaded = true; - } - } - - public async Task ReadComplexValueAsync(NodeId nodeId) - { - await EnsureTypesLoadedAsync(); - return await _session.ReadValueAsync(nodeId); - } + throw new InvalidOperationException("The server DataType could not be loaded."); } +DataValue value = await session.ReadValueAsync(variableNodeId); ``` +Coordinate concurrent loading at the application's session boundary rather than +using an unsynchronized Boolean flag. See [type system caching](#type-system-caching) +for reuse guidance. + ### Pattern 3: Reading Multiple Complex Values ```csharp public async Task ReadMultipleComplexValuesAsync(IList nodeIds) { // Load types once - var complexTypeSystem = new ComplexTypeSystem(session); + using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); await complexTypeSystem.LoadAsync(); // Read all values @@ -631,7 +677,7 @@ public async Task ReadMultipleComplexValuesAsync(IList nodeIds) ```csharp try { - var complexTypeSystem = new ComplexTypeSystem(session); + using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); bool success = await complexTypeSystem.LoadAsync(throwOnError: true); if (success) @@ -654,7 +700,7 @@ catch (ServiceResultException ex) ```csharp // Try to load a specific type -var complexTypeSystem = new ComplexTypeSystem(session); +using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); IType? systemType = await complexTypeSystem.LoadTypeAsync(typeNodeId, throwOnError: false); if (systemType == null) @@ -676,7 +722,7 @@ if (systemType == null) ```csharp // Load only enumerations (faster) -var complexTypeSystem = new ComplexTypeSystem(session); +using var complexTypeSystem = ComplexTypeSystem.Create(session, session.MessageContext.Telemetry); await complexTypeSystem.LoadAsync(onlyEnumTypes: true); // Or load only specific namespaces @@ -713,7 +759,7 @@ var response = await session.ReadAsync( ```csharp // Enable detailed logging through telemetry context var telemetry = session.MessageContext.Telemetry; -var complexTypeSystem = new ComplexTypeSystem(session, telemetry); +using var complexTypeSystem = ComplexTypeSystem.Create(session, telemetry); // The ComplexTypeSystem will log detailed information during type loading await complexTypeSystem.LoadAsync(); @@ -747,15 +793,37 @@ await complexTypeSystem.LoadAsync(); ## Server-Side Complex Types -Servers can build the same dynamic stand-in encodeables for the custom DataTypes in their address space. This is useful when a server loads a NodeSet2 at **runtime** whose DataTypes were never compiled into a .NET type: without a matching encodeable the server cannot encode or decode instances of those DataTypes. Server-side complex types prime the server's `IEncodeableFactory` with stand-ins built from the `DataTypeDefinition` attribute of every custom DataType, reusing exactly the same NativeAOT friendly path as the client (`ComplexTypeSystem`, in `Opc.Ua.Core.Schema`). This runs by default in `StandardServer` (controlled by `LoadComplexTypes`). +Servers can build the same dynamic stand-in encodeables for custom DataTypes +in their address space. This is useful when a server loads a NodeSet2 at +**runtime** and its DataTypes have no compiled .NET types. Without a matching +encodeable, the server cannot encode or decode instances of those DataTypes. -DataTypes that are already backed by a compiled, source-generated type are **already registered in the server's `IEncodeableFactory` and used as-is** for encoding and decoding; the server only builds stand-ins for the DataTypes that are still missing from the factory (i.e. those loaded from a NodeSet at runtime). +Server-side complex types prime the server's `IEncodeableFactory` with +stand-ins built from each custom DataType's `DataTypeDefinition` attribute. +This reuses the client's NativeAOT-friendly `ComplexTypeSystem` path in +`Opc.Ua.Core.Schema`. `StandardServer` runs this by default; control it with +`LoadComplexTypes`. + +The server uses compiled, source-generated DataTypes as-is because the +generator already registers them in `IEncodeableFactory`. It builds stand-ins +only for DataTypes missing from the factory, such as types loaded from a +NodeSet at runtime. If you are using [Runtime NodeSets](RuntimeNodeSets.md), the server-side complex-type pass runs automatically after startup imports and before each live lifecycle generation is published. No extra configuration is needed. See [RuntimeNodeSets.md](RuntimeNodeSets.md) for startup and live add/reload/remove semantics, compatible DataType rules, and the stream ownership contract. ### How compiled types reach the factory -Compiled DataTypes are registered explicitly, not by reflection: the OPC UA source generator emits one `Add(this IEncodeableFactoryBuilder)` extension per namespace, and a node manager calls it while it builds its address space (for example `Server.Factory.Builder.AddTestData().Commit()`). Node managers finish starting before `OnNodeManagerStartedAsync` runs, so every source-generated type is already present in `server.Factory` when the complex-type pass executes — `ComplexTypeSystem` finds them via `TryGetType` / `TryGetEncodeableType` and skips them, creating stand-ins only for the remaining runtime-loaded DataTypes. +Compiled DataTypes register explicitly, not through reflection. The OPC UA +source generator emits an `Add(this IEncodeableFactoryBuilder)` +extension for each namespace. A node manager calls the extension while it +builds its address space, for example +`Server.Factory.Builder.AddTestData().Commit()`. + +Node managers finish starting before `OnNodeManagerStartedAsync` runs. +Therefore, all source-generated types are already in `server.Factory` when +the complex-type pass executes. `ComplexTypeSystem` finds these types with +`TryGetType` or `TryGetEncodeableType` and skips them. It creates stand-ins +only for runtime-loaded DataTypes that remain. ### Configuring via dependency injection (recommended) @@ -771,7 +839,17 @@ builder.Services .AddComplexTypeSystem(); // build stand-ins for runtime-loaded DataTypes ``` -The pass runs once, after the address space is fully built and before the server starts accepting connections, so clients never observe a window where custom values cannot be decoded. The primed `IEncodeableFactory` is also exposed as the schema `IDataTypeDefinitionResolver` (via `EncodeableFactoryDefinitionSource`), so schemas can be produced directly from the factory — no separate registry population or address-space walk is required. If a `DataTypeDefinitionRegistry` is registered (for example by `AddSchemaGeneration()` for schema-only types that have no encodeable), it is composed as a fallback. +The pass runs once after the server builds its address space and before it +accepts connections. Clients therefore do not encounter custom values that +they cannot decode. + +The server also exposes the primed `IEncodeableFactory` as a schema +`IDataTypeDefinitionResolver` through `EncodeableFactoryDefinitionSource`. +This lets the server produce schemas directly from the factory without +populating a separate registry or walking the address space. If a +`DataTypeDefinitionRegistry` is registered, for example through +`AddSchemaGeneration()` for schema-only types without an encodeable, the +server composes it as a fallback. Options can be configured: diff --git a/docs/Concepts.md b/docs/Concepts.md new file mode 100644 index 0000000000..ce6e6063b9 --- /dev/null +++ b/docs/Concepts.md @@ -0,0 +1,258 @@ +# OPC UA concepts for SDK users + +This guide explains the ideas the rest of the documentation assumes. Read it +alongside [Getting started](GettingStarted.md) or immediately afterward. It +describes the concepts and the matching SDK types without detailing each +API. Each section links to the guide with complete contracts and examples. + +## Contents + +- [Applications](#applications) +- [Endpoints, secure channels, and sessions](#endpoints-secure-channels-and-sessions) +- [Application trust and user identity](#application-trust-and-user-identity) +- [The address space](#the-address-space) +- [Node identity and namespaces](#node-identity-and-namespaces) +- [Values, status codes, and timestamps](#values-status-codes-and-timestamps) +- [Service results and errors](#service-results-and-errors) +- [Subscriptions and monitored items](#subscriptions-and-monitored-items) +- [Events and alarms](#events-and-alarms) +- [Information models and companion specifications](#information-models-and-companion-specifications) +- [Client/server and PubSub](#clientserver-and-pubsub) +- [How the SDK is organized](#how-the-sdk-is-organized) +- [Where to go next](#where-to-go-next) + +## Applications + +An OPC UA application is a client, a server, or both. Each application +instance has an *application instance certificate* and an *ApplicationUri*. +The URI identifies that installed instance and must match the URI in its +certificate. A product URI identifies the product rather than one installation. + +In this SDK, you set the application name, URIs, and certificate-store root when +you configure a client or server. The hosting builders create the application +certificate on first start if the store does not contain one. + +## Endpoints, secure channels, and sessions + +A server offers one or more *endpoints*. An endpoint combines: + +- A URL, such as `opc.tcp://localhost:62541/MinimalBoilerServer`. +- A transport, such as UA-TCP (`opc.tcp`), HTTPS, or WebSockets. +- A *security mode*: `None`, `Sign`, or `SignAndEncrypt`. +- A *security policy*, such as `Basic256Sha256`, which selects the algorithms. +- The user-token types the server accepts. + +A client usually calls the discovery service `GetEndpoints` and selects the +endpoint whose security matches its requirements. The client then establishes +two layers: + +| Layer | Purpose | Authenticated by | +| --- | --- | --- | +| Secure channel | Signs and encrypts the messages between the two application instances. | Application instance certificates. | +| Session | Carries the user identity, request context, and subscriptions. | A user token, such as anonymous, user name, X.509, or an issued token. | + +A session can survive the loss of its channel: the client opens a new channel +and reactivates the same session. `ManagedSession` performs this recovery and, +if necessary, recreates the session. It is the recommended client entry point +for new applications. See [Sessions](Sessions.md) and [Transports](Transports.md). + +## Application trust and user identity + +Application trust and user identity are separate decisions: + +- **Application trust** decides whether this *software instance* may open a + secure channel. Each application keeps certificate stores: its own + certificates, *trusted* peers, *issuers* needed to validate chains, and + *rejected* certificates. An administrator approves a peer by placing its + certificate, or the CA that issued it, in the trusted store. +- **User identity** decides *who* is using the session and what that user may + do. The server authenticates the user token when the session is activated + and maps the identity to roles and permissions. + +Accepting unknown certificates automatically is a local-testing shortcut only; +it removes the protection against connecting to, or accepting, an impostor. +See [Certificates](Certificates.md), [Identity providers](IdentityProviders.md), +and [Role-based user management](RoleBasedUserManagement.md). + +## The address space + +A server exposes its data as an *address space*: a graph of *nodes* connected +by *references*. Each node has a *node class*: + +| Node class | Represents | +| --- | --- | +| Object | A device, machine, or other entity; also folders. | +| Variable | A value that can be read, written, or monitored. | +| Method | An operation that a client can call on an object. | +| ObjectType, VariableType | The definitions that instances are based on. | +| DataType, ReferenceType | The definitions of value types and relationship types. | +| View | A subset of the address space. | + +References have types. Hierarchical references, such as `Organizes`, +`HasComponent`, and `HasProperty`, form the tree you browse. Other references +describe relationships, such as `HasTypeDefinition`, which links an instance to +its type. Every server has a standard `Objects` folder (`ObjectIds.ObjectsFolder`) +that contains the `Server` object with status, capabilities, and diagnostics. + +A server-side *node manager* owns part of the address space and implements its +behavior. See [Node managers](NodeManagers.md). + +## Node identity and namespaces + +A `NodeId` combines a *namespace index* with an identifier, which is a number, +a string, a GUID, or an opaque byte string. The index refers to an entry in the +server's namespace table (`NamespaceArray`), which lists namespace URIs: + +- Index 0 is always the OPC UA namespace, `http://opcfoundation.org/UA/`. +- Index 1 is the server's own local namespace. +- Other indexes are assigned by the server and can differ between servers. + +Store and exchange the namespace *URI*, not the index. Resolve the index from +the connected session's namespace table (`session.NamespaceUris`) when needed. +An `ExpandedNodeId` can carry the URI itself. Generated model constants use +this form; convert one with `ExpandedNodeId.ToNodeId(id, session.NamespaceUris)`. +Standard node IDs such as `VariableIds.Server_ServerStatus_CurrentTime` are in +namespace 0 and need no conversion. + +A node's *BrowseName* is a `QualifiedName`: a name with a namespace index. +Clients can translate a browse path of names into a `NodeId`. + +`NodeId`, `QualifiedName`, `Variant`, `DataValue`, and several other built-in +types are value types in this SDK. Test them with `IsNull` rather than comparing +them with `null`. See [Improved type safety](migrate/2.0.x/types.md) for details. + +## Values, status codes, and timestamps + +Reading a variable returns a `DataValue`, which contains four parts: + +| Part | SDK member | Meaning | +| --- | --- | --- | +| Value | `WrappedValue` (`Variant`) | The value, or no value. | +| Status | `StatusCode` | Whether the value can be used. | +| Source timestamp | `SourceTimestamp` (`DateTimeUtc`) | When the value was produced at its source. | +| Server timestamp | `ServerTimestamp` (`DateTimeUtc`) | When the server last received or confirmed the value. | + +Always check the status before you use the value. A status is *Good*, +*Uncertain* (usable with caution), or *Bad* (do not use the value). Use +`StatusCode.IsGood`, `StatusCode.IsUncertain`, and `StatusCode.IsBad`, or compare +with a constant such as `status == StatusCodes.BadNodeIdUnknown`. + +A `Variant` holds any OPC UA built-in type, an array (`ArrayOf`), a matrix +(`MatrixOf`), or a structure (`ExtensionObject`). Extract a value with the +checked `TryGetValue` pattern: + +```csharp +if (!StatusCode.IsBad(value.StatusCode) && + value.WrappedValue.TryGetValue(out DateTimeUtc time)) +{ + Console.WriteLine($"{time} ({value.StatusCode})"); +} +``` + +All OPC UA date and time values are UTC and use `DateTimeUtc`. Binary data uses +`ByteString`. Server-defined structures can be decoded at run time with +[Complex types](ComplexTypes.md), or compiled with +[Source-generated data types](SourceGeneratedDataTypes.md). + +## Service results and errors + +Clients call *services*, such as `Read`, `Write`, `Browse`, and `Call`. Most +services process a list of operations and return one result per operation, so +one failed item does not fail the whole call. Check each result's status. + +A failure of the whole request, such as a timeout or a security error, raises a +`ServiceResultException`. Its `StatusCode` identifies the cause. +Convenience helpers can convert a Bad operation result into an exception; for +example, `ReadValueAsync` throws when the value's status is Bad. + +## Subscriptions and monitored items + +Polling with `Read` is inefficient for values that change. A *subscription* +lets the server report changes instead: + +1. The client creates a *subscription* in its session. The *publishing + interval* controls how often the server sends notifications. +2. The client adds *monitored items*. Each one watches a node attribute, usually + a variable's value, or an event source. The *sampling interval* controls how + often the server checks for changes; the *queue size* controls how many + changes it keeps between notifications. +3. The server sends data-change or event notifications, and *keep-alive* + messages when nothing has changed. If the client stops requesting + notifications, the subscription expires after its lifetime. + +The SDK offers callback subscriptions for long-lived application monitoring and +`IAsyncEnumerable` streams for short observations, such as waiting for a state +change. See [Subscriptions](Subscriptions.md). + +## Events and alarms + +Objects that are *event notifiers* report events, such as audit events or +state changes. A client subscribes to them with an event filter that selects the +event fields and types it needs. Alarms and conditions extend events with state +that operators acknowledge, confirm, or shelve. See +[Alarms and conditions](AlarmsAndConditions.md). + +## Information models and companion specifications + +An *information model* is a set of types and instances for a domain. The base +OPC UA model defines the server object, core types, and standard services. +*Companion specifications* build domain models on top of it, such as +Device Integration (OPC 10000-100) or Machinery (OPC 40001-1). + +Models are exchanged as *NodeSet2* XML files. This SDK can use a model in two +ways: + +- Generate typed code at build time, including a server node manager and typed + client proxies for object types. See [Node managers](NodeManagers.md). +- Load NodeSet2 files into a running server without generated code. See + [Runtime NodeSets](RuntimeNodeSets.md). + +Models depend on other models; for example, Device Integration depends on the +base model. See [Model dependencies](ModelDependencies.md). + +## Client/server and PubSub + +OPC UA defines two communication patterns: + +| | Client/server | PubSub | +| --- | --- | --- | +| Connection | A session between one client and one server. | No session; publishers send messages to a transport. | +| Interaction | Requests (read, write, browse, call) and subscriptions. | One-way dataset messages, plus optional actions. | +| Security | Secure channel plus per-session user identity. | Message security with group keys from a Security Key Service. | +| Transports | UA-TCP, HTTPS, WebSockets. | UDP, MQTT, Kafka, Ethernet. | + +In PubSub, a publisher's *DataSetWriter* turns a *published dataset* into +dataset messages, and a *WriterGroup* sends them in network messages at a +configured interval over a *PubSubConnection*. A subscriber's *DataSetReader* +selects messages by publisher, writer group, and dataset writer identifiers, +and decodes them with the dataset metadata. Messages use UADP (binary) or JSON +encoding. PubSub applications do not need a client or server. See +[PubSub](PubSub.md). + +## How the SDK is organized + +- **Packages.** Start with `OPCFoundation.NetStandard.Opc.Ua.Client` or + `OPCFoundation.NetStandard.Opc.Ua.Server`. Optional packages add transports, + complex types, PubSub, redundancy, and companion models. See + [Packages, platform support, and versioning](DeveloperGuide.md#packages-platform-support-and-versioning). +- **Hosting.** `services.AddOpcUa()` registers clients, servers, and features + with .NET dependency injection and the Generic Host. Direct construction, + such as `ManagedSessionBuilder` or `new StandardServer(telemetry)`, remains + available. See [Dependency injection](DependencyInjection.md). + In this documentation, *dependency injection* refers to the .NET pattern, + while *DI* in [Device Integration](DeviceIntegration.md) refers to OPC 10000-100. +- **Asynchronous APIs.** Operations are asynchronous and accept a + `CancellationToken`. Await them; do not block on their results. +- **Telemetry.** Components obtain loggers, metrics, and traces from an + `ITelemetryContext`. With hosting, the SDK adapts the host's logging. See + [Diagnostics](Diagnostics.md). +- **Ownership.** Dispose sessions, subscriptions, and certificate handles that + you create. `ManagedSession` and subscriptions support `await using`. + +## Where to go next + +Return to the [documentation index](README.md) and follow the path for your +role: a client, a server, or a PubSub application. If you have not yet run a +client and server, start with [Getting started](GettingStarted.md). To add your +own model to a server and use it from a client, continue with +[Your first information model](FirstModel.md). diff --git a/docs/ContainerReferenceServer.md b/docs/ContainerReferenceServer.md index 8a8b64df65..056be71208 100644 --- a/docs/ContainerReferenceServer.md +++ b/docs/ContainerReferenceServer.md @@ -2,17 +2,31 @@ ## Overview -There are multiple options to run the reference server in a Docker container: +Run the Reference Server in a Docker container using a published image or a +local build: -- Latest and release builds from the GitHub container registry [here](https://github.com/OPCFoundation/UA-.NETStandard/pkgs/container/uanetstandard%2Frefserver). These builds support only Linux targets. -- Since [this](https://github.com/OPCFoundation/UA-.NETStandard/commit/61a61081f6060804b12b8f351e32a8075703263d) commit the container image has amd64 and arm64 support. The build of the sample has been moved to containers, no more need to install the .NET SDK. -- Local build without .NET 6.0 SDK on Linux or Windows with Docker Desktop. The target OS is chosen based on the settings in Docker Desktop for Linux or Windows containers. -- Although with VS 2019 and greater there is built in Container support, so far issues in the UA Reference solution prevent build/startup/connection (under investigation). -- VS2022 supports native debugging on a Linux distribution with WSL. +- Pull the latest or a release image from the [GitHub Container Registry](https://github.com/OPCFoundation/UA-.NETStandard/pkgs/container/uanetstandard%2Frefserver). These images support Linux targets. +- The image supports `amd64` and `arm64`. The sample builds inside a container, so you do not need to install the .NET SDK. This change was introduced in [this commit](https://github.com/OPCFoundation/UA-.NETStandard/commit/61a61081f6060804b12b8f351e32a8075703263d). +- Build locally with Docker Desktop on Linux or Windows. Docker Desktop settings select the target container OS. +- Visual Studio 2019 and later include container support, but issues in the UA Reference solution currently prevent build, startup, or connection. +- Visual Studio 2022 supports native debugging of a Linux distribution through WSL. + +## Contents + +- [Overview](#overview) +- [Other published sample images](#other-published-sample-images) +- [Building the local containers](#building-the-local-containers) +- [Run the reference server container](#run-the-reference-server-container) + - [Run the local build of the Docker container](#run-the-local-build-of-the-docker-container) + - [Run the prebuilt Docker container hosted on GitHub](#run-the-prebuilt-docker-container-hosted-on-github) +- [Known limitations and issues](#known-limitations-and-issues) ## Other published sample images -In addition to the reference server (`refserver`), the [`Images CI`](../.github/workflows/docker-image.yml) workflow builds and publishes every other sample image — the sample servers, the device-integration pump server, the redundant client and both PubSub samples — to the GitHub container registry (`ghcr.io/opcfoundation/uanetstandard/`). All are `linux/amd64` + `linux/arm64`, and all are built by that one workflow: +The [`Images CI`](../.github/workflows/docker-image.yml) workflow builds +and publishes the sample images below to the GitHub Container Registry +(`ghcr.io/opcfoundation/uanetstandard/`). It builds each image for +`linux/amd64` and `linux/arm64`. | Image | Sample application | | --- | --- | @@ -29,11 +43,34 @@ In addition to the reference server (`refserver`), the [`Images CI`](../.github/ Image tags follow the [release-branch-only publication model](ReleaseProcess.md): -- **`:latest`, `:release`, and the exact `.` / `..` version tags** are updated only from a stable commit on a canonical `release/.` branch (see [Release process](ReleaseProcess.md)). A build from that branch that is not yet stable (still `-preview.N`) does **not** move these tags. +- **Stable tags** include `:latest`, `:release`, and version + tags such as `.` and `..`. Update them + only from a stable commit on a canonical `release/.` branch + (see [Release process](ReleaseProcess.md)). A preview build (`-preview.N`) + does **not** move these tags. - **`:latest-`** (for example `refserver:latest-master`) tracks the most recent development build on that branch. `Images CI` builds these from `master` and from `release/*` branches while they are pre-release. -- **`:`** (for example `refserver:2.0.0-preview.6` or `refserver:2.0.0`) always identifies the exact package version the image was built with, on every branch. +- **`:`** (for example `refserver:2.0.0-preview.6` or + `refserver:2.0.0`) identifies the exact package version used to build the + image, on any branch. -For example: `docker pull ghcr.io/opcfoundation/uanetstandard/ldsserver:latest` gets the most recently approved stable release; `docker pull ghcr.io/opcfoundation/uanetstandard/ldsserver:latest-master` gets the most recent development build. Each image has a Dockerfile under its application folder that is built from the repository root as context (for example `docker build -f samples/Lds/ConsoleLdsServer/Dockerfile -t opcua-lds-server .`). +For example, pull the most recently approved stable Lds server image with: + +```sh +docker pull ghcr.io/opcfoundation/uanetstandard/ldsserver:latest +``` + +Pull the most recent development build with: + +```sh +docker pull ghcr.io/opcfoundation/uanetstandard/ldsserver:latest-master +``` + +Each application folder contains a Dockerfile. Build it from the repository +root so the build can use the repository as context, for example: + +```sh +docker build -f samples/Lds/ConsoleLdsServer/Dockerfile -t opcua-lds-server . +``` ## Building the local containers @@ -49,21 +86,25 @@ On Linux, ## Run the reference server container -The following samples run the server in interactive mode, hostname is the same as the host, the certificate store, the log output and the configuration file (see option `-s`) are mapped to a folder called `./OPC Foundation`. +Run the container in interactive mode with the host's hostname. The +certificate store, log output, and configuration file (when you use `-s`) +are mounted in `./OPC Foundation`. -the following defaults are used: +The container uses these paths by default: -- the certificate store is mapped to './OPC Foundation/pki' -- A log file is created in './OPC Foundation/Logs' -- The shadow configuration file is created in './OPC Foundation/Quickstarts.ReferenceServer.Config.xml' +- Certificate store: `./OPC Foundation/pki` +- Log file: `./OPC Foundation/Logs` +- Shadow configuration: `./OPC Foundation/Quickstarts.ReferenceServer.Config.xml` -With the option `-s` the configuration file is first copied to the root of the mapped folders. In subsequent restarts the shadowed configuration file is used when the server is started and all settings can be changed from the mapped configuration file. +With `-s`, the container first copies the configuration file to the mounted +folder. On later restarts, the server reads this copy, so you can change the +settings there. ### Run the local build of the Docker container To run the local containers, batch files are provided called `dockerrun.bat` for Windows and `dockerrun.sh` for Linux. -### Run the prebuilt Docker container hosted on Github +### Run the prebuilt Docker container hosted on GitHub On Windows, open a command prompt and execute the following commands: diff --git a/docs/CryptoProvider.md b/docs/CryptoProvider.md index 40074afaa8..a1d73104c4 100644 --- a/docs/CryptoProvider.md +++ b/docs/CryptoProvider.md @@ -154,9 +154,10 @@ Purposes: A provider bound to a purpose it does not declare is **skipped**, and resolution falls through to the next candidate. A configuration mistake therefore fails close to its cause instead of deep inside a handshake. -Registration is explicit. There is no assembly scanning: it would make the effective security -configuration depend on what happens to be loaded, and it is incompatible with trimming and ahead-of-time -compilation. +Registration is explicit. The stack does not scan assemblies because that +would make the effective security configuration depend on which assemblies +happen to load. Assembly scanning would also conflict with trimming and +ahead-of-time compilation. ### Where resolution happens @@ -270,25 +271,31 @@ provider resolved for these cannot perform them and the platform would be used i ChannelSymmetric (ChaCha20Poly1305) for ...#ECC_nistP256_ChaChaPoly, KeyDerivation (HkdfSha256) for ... ``` -A module that covers AES-CBC but not ChaCha20-Poly1305 is therefore caught before it can run — where a -facet-only check would have reported it fully compliant while the platform performed every message on -any policy negotiating the algorithm it lacks. Nothing is reported when the platform provider is the one -resolved: the platform performing the platform's work is not a shortfall. +If a module supports AES-CBC but not ChaCha20-Poly1305, the audit catches it +before it runs. A facet-only check could mark the module compliant even when +the platform performs every message using a policy that requires the missing +algorithm. The audit reports a shortfall only when a non-platform provider is +resolved; it does not report the platform performing its own operations. Narrow the offered policy set, or extend the module, rather than lowering the compliance policy. ### Output a provider did not produce is refused -`IKeyDerivationProvider.DeriveKey` and `ISecureRandomSource.GetBytes` return `void`, so a module that -no-ops, fills part of the buffer, or swallows an internal failure is indistinguishable from one that -succeeded — and the buffer it was handed becomes channel signing keys, encryption keys, initialization -vectors and nonces. The platform paths these replace cannot fail this way: `Utils.PSHA` returns the array -it built and `RandomNumberGenerator` throws. +`IKeyDerivationProvider.DeriveKey` and `ISecureRandomSource.GetBytes` return +`void`. A provider could silently fail by returning without writing output, +filling only part of the buffer, or swallowing an internal failure. The caller +cannot distinguish those cases from success, and the buffer becomes channel +signing keys, encryption keys, initialization vectors, and nonces. -That matters most for exactly the deployments this seam exists for. A network- or hardware-served module -can be transiently offline, and because both ends of a channel usually run the same image, both would -derive the same dead key material and the handshake would complete — traffic flowing with no -confidentiality and forgeable integrity, invisible to the operator and to the peer. +The platform paths these providers replace do not fail this way: +`Utils.PSHA` returns the array it creates, and `RandomNumberGenerator` throws +on failure. + +This risk matters most for the deployments that use this seam. A network- or +hardware-served module can go offline temporarily. If both ends of a channel +run the same image, they could derive the same invalid key material and +complete the handshake. Traffic would then lack confidentiality and have +forgeable integrity, without either peer detecting the failure. So the buffer is stamped before the call and checked after it. Output left untouched or zeroed is rejected with `BadSecurityChecksFailed` rather than used. This cannot prove the output is good — no @@ -304,7 +311,7 @@ directly. | Facet | Covers | |---|---| -| `ISymmetricCryptoProvider` | AES-CBC, AES-GCM, ChaCha20-Poly1305 and the HMAC signatures | +| `ISymmetricCryptoProvider` | Advanced Encryption Standard (AES) in CBC and GCM modes, ChaCha20-Poly1305, and Hash-based Message Authentication Code (HMAC) signatures | | `IKeyDerivationProvider` | P_SHA1, P_SHA256, HKDF-SHA256, HKDF-SHA384 | | `ISecureRandomSource` | Nonces and other random material | @@ -356,20 +363,21 @@ cost of the indirection is measured rather than assumed. ### A provider that cannot do what it was bound to -Binding a provider to `ChannelSymmetric` without implementing `ISymmetricCryptoProvider` would otherwise -be silent: resolution falls through to the platform and the channel keeps working, while a deployment -believes its validated module performed the per-message cryptography. +Binding a provider to `ChannelSymmetric` without implementing +`ISymmetricCryptoProvider` would fail silently. Resolution would fall back to +the platform, while the deployment could believe its validated module +performed the per-message cryptography. -`CryptoCompliance.GetUnservedOperationPurposes` reports exactly that case, and under `FipsOnly` -`CryptoProviderAuditor.ThrowIfNotCompliant()` refuses to start rather than run on cryptography the -operator did not ask for. +`CryptoCompliance.GetUnservedOperationPurposes` reports this case. Under +`FipsOnly`, `CryptoProviderAuditor.ThrowIfNotCompliant()` stops startup rather +than run cryptography the operator did not request. ## Using a key served over a network -`RSA` and `ECDsa` are synchronous contracts, and they are .NET's rather than this stack's, so a key -backed by a cloud key service occupies a thread for the whole of every call. Rather than replace those -contracts — which would make every ready-made hardware and cloud implementation unusable — an -implementation may **also** declare an asynchronous path: +`RSA` and `ECDsa` are synchronous .NET contracts. A key backed by a cloud +service therefore occupies a thread for each call. The stack keeps these +contracts so existing hardware and cloud implementations remain usable. +Providers may **also** declare an asynchronous path: ```csharp public sealed class KmsRsa : RSA, IAsyncRsaKey @@ -442,11 +450,10 @@ new PubSubApplicationBuilder(telemetry) ### What device custody can and cannot mean here -**With a standard Security Key Service the key necessarily exists in process -memory.** `GetSecurityKeys` (Part 14 §8.3.2) returns raw key bytes over the wire, -so the property the client and server side achieve — the key never leaves the -device — **cannot** be achieved for PubSub through the SKS pull profile. That is -a property of the specification, not of this stack. +**With a standard Security Key Service, the key necessarily exists in process +memory.** `GetSecurityKeys` (Part 14 §8.3.2) returns raw key bytes over the +wire. Therefore, the SKS pull profile cannot keep PubSub keys exclusively on +the device. This limitation comes from the specification, not the stack. Introducing a wrapped-key envelope would change what is on the wire and break interoperability with third-party key services and publishers, so it is @@ -459,9 +466,9 @@ What is achievable, and is supported: `IPubSubSecurityKeyProvider` is the seam; an implementation may derive per-token keys from a long-lived secret that stays in a device, so only the derived material is in memory. -- **Its lifetime is bounded.** `PubSubSecurityKey` zeroizes on disposal, the key - ring disposes keys as it retires them, and the intermediate copies made while - unpacking an SKS response are cleared rather than left in the heap. +- **Its lifetime is bounded.** `PubSubSecurityKey` zeroizes key material on + disposal. The key ring disposes keys as it retires them. The implementation + also clears intermediate copies created while unpacking an SKS response. ## Contributing a security policy @@ -488,11 +495,14 @@ services.AddOpcUa() .AddSecurityPolicy(customPolicy); ``` -A registered policy is discoverable through the same API as a built-in one — `GetInfo`, `GetUri`, -`GetDisplayName`, `GetDisplayNames` and the default-URI helpers — because those are now driven from one -table rather than from reflection over the constants. Registering a URI or name that already exists -throws unless `replaceExisting: true` is passed, which makes shadowing a built-in policy deliberate and -reversible: disposing the returned registration restores what was there before. +A registered policy uses the same discovery API as a built-in policy: +`GetInfo`, `GetUri`, `GetDisplayName`, `GetDisplayNames`, and the default-URI +helpers. One table drives these lookups instead of reflection over constants. + +Registration throws if the URI or name already exists. Pass +`replaceExisting: true` to replace it. Dispose the returned registration to +restore the previous policy, making built-in policy shadowing deliberate and +reversible. ### Resolving the policy set @@ -520,12 +530,14 @@ Passing nothing keeps the previous behaviour and uses `SecurityPolicies.Default` ### Reaching the secure channel and the session -An application composed through the builder gets its registry threaded through the stack: the client -channel manager, the session factory, the sessions it creates and the identity providers they select -all resolve policies against it, and a server's listeners are opened with it. A policy registered with -`AddSecurityPolicy` is therefore negotiable by that application's channels — the secure channel resolves -the handshake policy, its key sizes, its asymmetric padding and its signature algorithm from the same -registry. +The builder passes the registry through the application stack. The client +channel manager, session factory, sessions, and selected identity providers +all resolve policies against it. The server also opens its listeners with +that registry. + +A policy registered with `AddSecurityPolicy` is therefore negotiable by the +application's channels. The secure channel uses the same registry to resolve +the handshake policy, key sizes, asymmetric padding, and signature algorithm. Constructing the transport directly carries the registry the same way: @@ -556,8 +568,8 @@ ManagedSession session = await new ManagedSessionBuilder(configuration, telemetr .ConnectAsync(ct); ``` -The registry then reaches both the sessions the builder creates and the channels they open, including -the ones re-created on reconnect. +The builder passes the registry to the sessions it creates and the channels +they open, including channels recreated during reconnect. The registry a container builds is **its own**. A policy registered by one application is not visible to another hosted in the same process, and it is not visible to `SecurityPolicies.Default`. That @@ -585,12 +597,14 @@ policy whose algorithms are unavailable is filtered out the same way the built-i advertised, and a consumer can light them up from outside without rebuilding the stack — which is what `RegisterLightsUpCurvePoliciesFromOutsideCore` asserts. -Registering a policy makes it **advertised and resolvable**; it does not by itself supply the -cryptography behind it. For the two curve profiles above, the in-tree key agreement is still behind a -compile-time symbol that no project defines and a BouncyCastle dependency, so a deployment that lights -them up supplies the operations through a provider, exactly as [the sections above](#substituting-the-symmetric-primitives) -describe. That is the intended division: the policy set says *what* is offered, the provider says *who* -performs it. +Registering a policy makes it **advertised and resolvable**, but does not +supply its cryptography. The in-tree key agreement for the two curve profiles +above remains behind a compile-time symbol that no project defines, and it +depends on BouncyCastle. A deployment that enables these profiles must supply +their operations through a provider, as described in +[Substituting the symmetric primitives](#substituting-the-symmetric-primitives). +The policy set says *what* is offered; the provider says *who* performs the +cryptography. Removing the reflection that used to build these tables also removed the last reflection in `Opc.Ua.Core.Security.Constants`, which is why this is also a trimming and Native AOT improvement. diff --git a/docs/DependencyInjection.md b/docs/DependencyInjection.md index df629e83b9..ca192de02a 100644 --- a/docs/DependencyInjection.md +++ b/docs/DependencyInjection.md @@ -1,9 +1,12 @@ # Dependency Injection -This document describes the unified `Microsoft.Extensions.DependencyInjection` -surface for the OPC UA .NET Standard libraries. The surface is rooted in -a single `services.AddOpcUa()` call that returns an `IOpcUaBuilder` on -which every feature library hangs its own fluent `.AddXxx(...)` extension. +This guide describes the unified `Microsoft.Extensions.DependencyInjection` +surface for OPC UA .NET Standard libraries. Start with +`services.AddOpcUa()`, which returns an `IOpcUaBuilder`. Feature libraries +extend the builder with their own fluent `.AddXxx(...)` methods. +For a complete, runnable client and server, see +[Getting started](GettingStarted.md); this guide is the reference for the +builder extensions. The dependency injection surface is consistent across: @@ -23,12 +26,59 @@ The dependency injection surface is consistent across: `src/Opc.Ua.PubSub.Server`) — see [`PubSub.md`](PubSub.md) for the full library reference. -The non-dependency-injection public constructors and factories of every library -(`new ApplicationInstance(telemetry)`, `new StandardServer(telemetry)`, -`new LdsServer(telemetry)`, `new ManagedSession(...)` etc.) remain -unchanged. Use dependency injection when you want the .NET Generic Host to own application -lifetime, logging, and configuration; use the manual constructors when -you need finer control. +Public constructors and factories remain available for applications that do +not use dependency injection. Examples include `new ApplicationInstance(telemetry)`, +`new StandardServer(telemetry)`, `new LdsServer(telemetry)`, and +`new ManagedSession(...)`. Use dependency injection when you want the .NET +Generic Host to manage application lifetime, logging, and configuration. Use +the manual constructors when you need finer control. + +## Contents + +- [Quick reference](#quick-reference) +- [Root: `services.AddOpcUa()`](#root-servicesaddopcua) + - [Buffer managers](#buffer-managers) +- [Shared application configuration](#shared-application-configuration) + - [Application identity and certificate defaults](#application-identity-and-certificate-defaults) + - [Advanced application certificate validation](#advanced-application-certificate-validation) +- [Options binding](#options-binding) +- [Server feature](#server-feature) + - [Server metadata](#server-metadata) + - [Node-manager factories](#node-manager-factories) + - [Post-startup tasks](#post-startup-tasks) + - [Alias-name stores and standard browse nodes](#alias-name-stores-and-standard-browse-nodes) + - [Localized resources](#localized-resources) + - [Migrating with an existing configuration XML file](#migrating-with-an-existing-configuration-xml-file) + - [Server security and resource controls](#server-security-and-resource-controls) + - [First-class server options](#first-class-server-options) + - [Committed session bindings](#committed-session-bindings) + - [Server-side reverse connect](#server-side-reverse-connect) + - [Operation limits](#operation-limits) + - [User token policies](#user-token-policies) + - [Identity (server)](#identity-server) + - [Fluent shortcuts and one-shot presets](#fluent-shortcuts-and-one-shot-presets) + - [Positioning](#positioning) + - [Robotics](#robotics) +- [Client feature](#client-feature) + - [Client: using an existing configuration XML file](#client-using-an-existing-configuration-xml-file) + - [Client: loading the configuration eagerly](#client-loading-the-configuration-eagerly) + - [Fluent shortcuts](#fluent-shortcuts) + - [Identity (client)](#identity-client) +- [Complex types](#complex-types) +- [Alarms and conditions](#alarms-and-conditions) +- [Client-side reverse connect](#client-side-reverse-connect) +- [Channel manager](#channel-manager) +- [Application instance (advanced)](#application-instance-advanced) +- [GDS Client](#gds-client) +- [GDS Server](#gds-server) + - [Identity (GDS server)](#identity-gds-server) +- [LDS Server](#lds-server) +- [WoT Connectivity Server](#wot-connectivity-server) +- [WoT Connectivity Client](#wot-connectivity-client) +- [Combined hosts](#combined-hosts) +- [Native AOT](#native-aot) +- [Telemetry](#telemetry) +- [See also](#see-also) ## Quick reference @@ -231,12 +281,15 @@ When both features are registered, the shared configuration has `ApplicationType | `PkiRoot` | A per-application `OPC Foundation/{ApplicationName}/pki` directory below the process temporary directory. Configure a persistent, access-controlled location in production. | | Application certificates and stores | Directory-backed application, trusted peer/issuer, HTTPS, user, and rejected stores are created below `PkiRoot`; default RSA and supported ECC application-certificate identifiers are selected. | -The security builder starts with secure defaults: unknown certificates are not -auto-accepted, the application certificate is not copied into a shared trusted -store, SHA-1 certificates and unknown revocation status are rejected, nonce -validation errors are not suppressed, certificate chains are sent, the minimum -RSA key size is 2048, and at most five rejected certificates are retained. -Validated-certificate caching is off unless enabled explicitly. +The security builder uses these secure defaults: + +- It does not auto-accept unknown certificates or copy the application + certificate into a shared trusted store. +- It rejects SHA-1 certificates and unknown revocation status. +- It does not suppress nonce-validation errors. +- It sends certificate chains and requires an RSA key size of at least 2048. +- It retains at most five rejected certificates. +- It disables validated-certificate caching unless explicitly enabled. ### Advanced application certificate validation @@ -642,21 +695,24 @@ subclass works too: builder.Services.AddOpcUa().AddServer("MyServer.Config.xml"); ``` -On this path the file is authoritative: the `OpcUaServerOptions` knobs -that feed the configuration builder (`ApplicationName`, `EndpointUrls`, -`PkiRoot`, policy toggles, transport quotas, `ReverseConnect`, -`ConfigureBuilder`, ...) -are not applied, and the file also takes precedence over a shared -application registered with `ConfigureApplication(...)`. Options that -act on the hosted server itself (`Identity`, `ConfigureRateLimits`) and -runtime registrations (`AddNodeManager`, `AddNodeManagers`, startup tasks, -authenticators, metadata, resources, and alias settings) keep working. -Configuration-building shortcuts such as `AddReverseConnect` and -`ConfigureOperationLimits` do not override file settings. To override -individual file settings from code, use `ConfigureLoadedConfiguration`, -also exposed as the optional `AddServer` callback below. It runs after -the file is loaded and validated but before certificates are checked -and the server starts: +On this path, the file is authoritative and takes precedence over any +application registered with `ConfigureApplication(...)`. The file also +overrides configuration-builder settings from `OpcUaServerOptions`, including: + +- `ApplicationName`, `EndpointUrls`, and `PkiRoot` +- Security-policy toggles and transport quotas +- `ReverseConnect` and `ConfigureBuilder` + +Hosted-server settings such as `Identity` and `ConfigureRateLimits` still +apply, as do runtime registrations such as node managers, startup tasks, +authenticators, metadata, resources, and alias settings. Configuration +shortcuts such as `AddReverseConnect` and `ConfigureOperationLimits` do not +override file settings. + +To override individual file settings in code, use +`ConfigureLoadedConfiguration`, also available through the optional +`AddServer` callback below. It runs after the file is loaded and validated, +but before the server checks certificates or starts: ```csharp builder.Services @@ -1878,7 +1934,7 @@ services.AddOpcUa() - [Sessions](Sessions.md) — `ManagedSession`, reconnect, subscription engines. - [Source Generated NodeManagers](NodeManagers.md#source-generated-node-managers) — `IAsyncNodeManagerFactory` from a model design XML. -- [Native AOT](NativeAoT.md) — AOT testing setup. +- [Native AOT](NativeAoT.md) — publishing applications and the AOT test harness. - [GDS Developer Guide](GDS.md) — GDS service interfaces and provider patterns. - [Robotics](Robotics.md) — OPC 40010 hosting, model providers, and topology builders. - [WoT Connectivity](WoTConnectivity.md) — OPC 10100-1 information model. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 9a542dbfc5..306ac75bfd 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -4,6 +4,32 @@ This guide is the starting point for contributing to the OPC UA .NET Standard st If you are new here, read the sections in order: [Prerequisites](#prerequisites) → [Repository layout](#repository-layout) → [Building](#building) → [Running tests](#running-tests) → [Coding standards](#coding-standards-dos-and-donts). The [How-to guides](#how-to-guides) and [Packages, platform support, and versioning](#packages-platform-support-and-versioning) sections are reference material you can jump to as needed. +## Contents + +- [Prerequisites](#prerequisites) +- [Repository layout](#repository-layout) +- [Building](#building) +- [Running tests](#running-tests) +- [Coding standards (dos and don'ts)](#coding-standards-dos-and-donts) +- [How-to guides](#how-to-guides) + - [Add a log message (source-generated)](#add-a-log-message-source-generated) + - [Other common tasks](#other-common-tasks) +- [Packages, platform support, and versioning](#packages-platform-support-and-versioning) + - [Released packages](#released-packages) + - [Supported target frameworks](#supported-target-frameworks) + - [Supported analyzer and source generator hosts](#supported-analyzer-and-source-generator-hosts) + - [Versioning](#versioning) +- [Continuous integration](#continuous-integration) + - [What runs where](#what-runs-where) + - [Test tiers](#test-tiers) + - [Running the full scope](#running-the-full-scope) + - [Required checks and coverage](#required-checks-and-coverage) + - [Reproducing a CI leg locally](#reproducing-a-ci-leg-locally) + - [Triggering a pipeline run on a pull request](#triggering-a-pipeline-run-on-a-pull-request) + - [Migration status](#migration-status) +- [Contributing and pull requests](#contributing-and-pull-requests) +- [Related documentation](#related-documentation) + ## Prerequisites - **.NET SDK 10.0** — the whole repository builds and restores with the [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0). Older SDKs are not supported for building `main`. The class libraries still *target* older frameworks (see [Packages, platform support, and versioning](#packages-platform-support-and-versioning)), but you build them with the .NET 10 SDK. @@ -277,7 +303,7 @@ The analyzer and source generator packages ship under `analyzers/dotnet/roslyn **Adding a band below 4.14 is not just another entry in that file.** The analyzer closure — the generator, `Opc.Ua.SourceGeneration.Core` **and** `Opc.Ua.Types` — must bind against the Roslyn host's own `System.Collections.Immutable` and `System.Reflection.Metadata`. .NET satisfies a reference from a *higher* assembly version but never from a lower one, and those assemblies are supplied by the compiler, so the closure must reference the lowest version across every supported band and must never ship a copy of its own. Roslyn 4.14 and 5.0 both depend on 9.0.0, which is why `$(RoslynRuntimeVersion)` in `roslyn.props` drives the central pin and one build of the non-Roslyn closure serves both bands. Going lower — Roslyn 4.8 wants 7.x — would mean building that whole closure, `Opc.Ua.Types` included, a second time. -> + > Get it wrong and the failure is silent: the generator is skipped (`CS9057`), fails to load (`CS8032`) or throws `MissingMethodException` while initializing (`CS8784`) — all *warnings*, so the consumer just gets no generated code. `validate-source-generator-packages.ps1` therefore refuses any package that ships `Microsoft.CodeAnalysis*`, `System.Collections.Immutable` or `System.Reflection.Metadata`, and runs the packed down-level payload through a real compiler of that band. ### Versioning diff --git a/docs/DeviceIntegration.md b/docs/DeviceIntegration.md index e8fa643dc7..6c50f33781 100644 --- a/docs/DeviceIntegration.md +++ b/docs/DeviceIntegration.md @@ -24,6 +24,7 @@ plugs it together. - [Software update](#software-update) - [Client helpers](#client-helpers) - [What is supported (OPC 10000-100)](#what-is-supported-opc-10000-100) +- [See also](#see-also) ## Library layout @@ -141,13 +142,13 @@ fluent API for node managers. > `InstanceNamespaceIndex`, the server application's namespace derived > from `ApplicationConfiguration.ApplicationUri`, rather than in the > DI or companion-spec namespace of the parent. This follows the -> companion-spec boilerplate rule (for example OPC 10000-100 §13.4 and -> OPC 40223) that nodes not defined by a specification do not use the -> standard namespace; the Local Server URI namespace is the namespace -> for nodes defined by the local server. BrowseNames defined by DI or a -> companion specification keep their specification namespace, while -> application-defined instance names, functional groups, and properties -> should use `InstanceNamespaceIndex`. +> companion-spec modelling rules (for example OPC 10000-100 §13.4 and +> OPC 40223): nodes not defined by a specification must not use the +> standard namespace. The local server defines such nodes in its Local +> Server URI namespace. BrowseNames defined by DI or another companion +> specification keep that specification's namespace. Use +> `InstanceNamespaceIndex` for application-defined instance names, +> functional groups, and properties. ### Entry points @@ -191,7 +192,7 @@ ITopologyElementBuilder TopologyElementByBrowseName( where TElement : TopologyElementState; ``` -`CreateDeviceAsync` performs four steps: +`CreateDeviceAsync` performs five steps: 1. Resolves the parent (default: Device Integration `DeviceSet`; subclasses override `ResolveDefaultDeviceParent()` — e.g. machinery managers can return @@ -200,21 +201,22 @@ ITopologyElementBuilder TopologyElementByBrowseName( retain `HasComponent`. 3. Fails fast if a child with the same browse name already exists (`StatusCodes.BadBrowseNameDuplicated`). -4. Materialises the instance through the source-generated - `CreateInstanceOf` factory (e.g. `CreateInstanceOfDeviceType`) - so the device carries the type's **mandatory** children — for - `DeviceType` the eight nameplate variables (`Manufacturer`, `Model`, +4. Materializes the instance through the source-generated + `CreateInstanceOf` factory, such as + `CreateInstanceOfDeviceType`. The factory adds the type's **mandatory** + children and `HasInterface` references. For `DeviceType`, the children + are eight nameplate variables: `Manufacturer`, `Model`, `HardwareRevision`, `SoftwareRevision`, `DeviceRevision`, - `DeviceManual`, `SerialNumber`, `RevisionCounter`) with correct - DI-namespace BrowseNames — plus the type's `HasInterface` - references. Because a browse name is supplied, the factory also - rebases the whole subtree onto per-instance NodeIds in the server's - application namespace (`DiNodeManager.InstanceNamespaceIndex`, the - Local Server URI namespace), so multiple instances of the - same type never collide on the TYPE NodeIds emitted by the generator - and never mint application nodes in DI or other companion-spec - namespaces. It then - sets BrowseName/SymbolicName/DisplayName and stamps the + `DeviceManual`, `SerialNumber`, and `RevisionCounter`. It assigns + each variable a DI-namespace BrowseName. + + Because the factory receives a BrowseName, it also rebases the subtree + onto per-instance NodeIds in the server's application namespace + (`DiNodeManager.InstanceNamespaceIndex`, the Local Server URI + namespace). This prevents instances of the same type from colliding + on the generator's type NodeIds. It also keeps application nodes out + of DI and other companion-spec namespaces. Finally, the factory sets + `BrowseName`, `SymbolicName`, and `DisplayName`, and stamps `TypeDefinitionId`. 5. Calls the real `AsyncCustomNodeManager.AddPredefinedNodeAsync` so the node create lifecycle, subscription wiring, type-tree registration, @@ -291,11 +293,11 @@ their in-type `MethodDeclarationId`s. The model is intentionally **lean** (`IVendorNameplateType`/`ITagNameplateType`/`ISupportInfoType` members such as `AssetId`, `ProductCode`, `DeviceTypeImage`, `Documentation`, `ImageSet`, `ProtocolSupport`) to be instantiated, so they are omitted -unless the application adds them. A strict external compliance checker -may still flag those optional members or report placeholder warnings for -application-added `HasComponent` children (e.g. a custom `Diagnostics` -functional group or the `SoftwareUpdate` facet); these are permitted -omissions/extensions rather than modelling errors. +unless the application adds them. An external compliance checker may still flag omitted optional members. +It may also report placeholder warnings for application-added +`HasComponent` children, such as a custom `Diagnostics` functional group +or the `SoftwareUpdate` facet. The specification permits these omissions +and extensions; they are not modelling errors. #### Functional groups @@ -452,8 +454,8 @@ device.WithSupportInfo(info => ## Hosting integration -`AddOpcUaDi()` and `ConfigureDevicesFor()` plug the -Device Integration (DI) library into the unified `AddOpcUa()` +Use `AddOpcUaDi()` and `ConfigureDevicesFor()` to add the +Device Integration library to the unified `AddOpcUa()` `Microsoft.Extensions.DependencyInjection` hosting pattern. ### Server-side surface @@ -775,121 +777,54 @@ Then attach the session-close hook from inside a ## Software update -The software-update facet exposes a package-storage layer plus a -minimal client helper for OPC 10000-100 §10.3. The full state-machine -wiring (PrepareForUpdate / Installation / PowerCycle / Confirmation) -remains application-specific — the source generator emits typed -`*StateMachineState` proxies that applications drive directly when -needed. +The software-update facet implements OPC 10000-100 workflows for loading, +preparation, installation, power cycle, and confirmation. Attach it with +`WithSoftwareUpdate`; application handlers supply device-specific behavior. +The SDK supplies the state-machine wiring and file-transfer infrastructure. ### Server-side: package store -The store is an application-facing abstraction over the binary -artifacts that the Device Integration software-update facet exposes. Two -implementations ship in `Opc.Ua.Di.Server.SoftwareUpdate`: - -| Type | Backing | Use case | -|------|---------|----------| -| `MemoryPackageStore` | `ConcurrentDictionary` | Unit tests; small fixtures. | -| `FileSystemPackageStore` | `Opc.Ua.Server.FileSystem.IFileSystemProvider` | Production — reuses the same provider model used by the server's `FileSystem` mount. | +`ISoftwarePackageStore` stores package metadata and payloads. +`ISoftwareFolder` maintains each device's Current / Previous / Future versions. +Both have memory and file-system implementations. See +[Software Update storage abstractions](SoftwareUpdate.md#storage-abstractions) +for their contracts and persistence choices. #### Surface -```csharp -public interface ISoftwarePackageStore -{ - IAsyncEnumerable ListAsync(CancellationToken ct = default); - ValueTask GetAsync(string packageId, CancellationToken ct = default); - ValueTask ExistsAsync(string packageId, CancellationToken ct = default); - ValueTask OpenReadAsync(string packageId, CancellationToken ct = default); - ValueTask AddAsync(SoftwarePackage metadata, Stream payload, CancellationToken ct = default); - ValueTask DeleteAsync(string packageId, CancellationToken ct = default); -} -``` - -`SoftwarePackage` is a record carrying `Id`, `Version`, `Vendor`, -`Description`, `SizeBytes`, `CreatedAt`, `Hash`. Both stores -recompute `SizeBytes` and `CreatedAt` during `AddAsync` so callers -can pass zeros / `default` in the input metadata. +Use the [package-store contract](SoftwareUpdate.md#package-store-contract) +for operations, metadata, and stream ownership. The Software Update guide also +owns `WithSoftwareUpdate` options and handler hooks. #### `FileSystemPackageStore` layout -Each package is stored as a directory containing two files: - -``` -{root}/ - {package-id}/ - payload.bin ← the binary firmware/installer - metadata.json ← the SoftwarePackage record as JSON -``` - -JSON serialization uses a source-generated `System.Text.Json` -context (`SoftwarePackageJsonContext`) so the store is AOT-friendly. - -Package IDs must NOT contain `/` or `\` — the store validates and -throws `ArgumentException` to prevent path traversal. +Use the store through `ISoftwarePackageStore`, rather than modifying its backing +files directly. See [file-system layout and provider composition](SoftwareUpdate.md#file-system-layout-and-provider-composition) +for the file-system provider and persistence options. #### Composing with `IFileSystemProvider` -```csharp -IFileSystemProvider fs = new PhysicalFileSystemProvider( - rootDirectory: "/var/lib/myserver/packages", - mountName: "Packages", - isWritable: true); - -ISoftwarePackageStore store = new FileSystemPackageStore( - provider: fs, - rootPath: "/SoftwarePackages"); -``` - -The same `IFileSystemProvider` instance can also be mounted into the -server's address space via `FileSystemNodeManager` — both paths -share the on-disk layout. +Reuse the configured provider as described in +[provider composition](SoftwareUpdate.md#file-system-layout-and-provider-composition). ### Hosting -Register the store as a singleton and seed it from a -`ConfigureDevicesFor` configurator: - -```csharp -builder.Services.AddSingleton(); -builder.Services - .AddOpcUa() - .AddServer(o => { ... }) - .AddNodeManager() - .ConfigureDevicesFor(async ctx => - { - ISoftwarePackageStore store = ctx.GetRequiredService(); - await store.AddAsync( - new SoftwarePackage( - Id: "firmware-1.0.0", - Version: "1.0.0", - Vendor: "Acme", - Description: "Initial firmware", - SizeBytes: 0, - CreatedAt: default, - Hash: string.Empty), - new FileStream("/path/to/firmware.bin", FileMode.Open)); - }); -``` +Register the package store with the host and attach it to the device's +software-update facet. See the [hosted-server walkthrough](SoftwareUpdate.md#hosted-server-walkthrough) +and [package seeding example](SoftwareUpdate.md#seeding-a-package). ### Client-side software update -`SoftwareUpdateClient` exposes a minimal read-only surface: +`SoftwareUpdateClient` reads versions, uploads packages, and exposes typed +state-machine operations. The usual workflow is upload, prepare, install, and +confirm; supported steps depend on the device's advertised update capabilities. +See [uploading a package](SoftwareUpdate.md#uploading-a-package) and +[typed state-machine operations](SoftwareUpdate.md#typed-part-16-state-machine-surface) +for runnable call sequences. -```csharp -public sealed class SoftwareUpdateClient -{ - public SoftwareUpdateClient(ISession session, NodeId softwareUpdateNodeId, ITelemetryContext telemetry); - public ValueTask ReadSoftwareVersionAsync(CancellationToken ct = default); -} -``` - -Method-level invocation (Loading, Installation, ...) is performed -through the typed `*MethodStateClient` proxies emitted by the source -generator for the Device Integration model. The client integration registers the -factory `Func>` -via `services.AddOpcUa().AddClient(...).AddOpcUaDi()`. +The DI client integration registers +`Func>` through +`services.AddOpcUa().AddClient(...).AddOpcUaDi()`. ## Client helpers @@ -1024,7 +959,10 @@ References are to the OPC 10000-100 (DI v1.05) specification sections. - `TopologyElementType` (§5.2) — abstract base exposed through `ITopologyElementBuilder`. -- `IVendorNameplateType` (§5.10) — Manufacturer, Model, SerialNumber, HardwareRevision, SoftwareRevision, DeviceRevision, DeviceManual, DeviceClass, ProductInstanceUri, ProductCode. Populated by `IDeviceBuilder.WithIdentification(...)`. +- `IVendorNameplateType` (§5.10) exposes `Manufacturer`, `Model`, + `SerialNumber`, `HardwareRevision`, `SoftwareRevision`, `DeviceRevision`, + `DeviceManual`, `DeviceClass`, `ProductInstanceUri`, and `ProductCode`. + Populate these properties with `IDeviceBuilder.WithIdentification(...)`. - `ITagNameplateType` (§5.11) — AssetId, ComponentName, DeviceRevision. Populated by the same builder. - `IDeviceHealthType` (§5.12) — DeviceHealth enum plus the four NAMUR alarm references. - `IAssetLocationIndicationType` (§5.13) — `StartLocationIndication` / `StopLocationIndication` methods. @@ -1063,7 +1001,11 @@ Custom groups go through `WithFunctionalGroup(qualifiedName, action)`. ### Lock service (§10.5) -- `LockingServicesType` and the four method types (`InitLockMethodType`, `RenewLockMethodType`, `ExitLockMethodType`, `BreakLockMethodType`) — wired through `ILockService` and `DefaultLockService` (session ownership, configurable timeout, automatic cleanup on session close). +- `LockingServicesType` and four method types: + `InitLockMethodType`, `RenewLockMethodType`, `ExitLockMethodType`, and + `BreakLockMethodType`. `ILockService` and `DefaultLockService` wire these + methods and provide session ownership, configurable timeouts, and cleanup + when a session closes. ### Software update (§10.3) @@ -1071,7 +1013,10 @@ Custom groups go through `WithFunctionalGroup(qualifiedName, action)`. - `SoftwareLoadingType`, `PackageLoadingType` (§10.3.4) — abstract bases. - `DirectLoadingType`, `CachedLoadingType`, `FileSystemLoadingType` (§10.3.4) — the three loading variants. - `SoftwareVersionType` (§10.3.6) — Manufacturer, ProductInstanceUri, SoftwareRevision, PatchIdentifiers, ReleaseDate, ChangeLog, Hash. -- `PrepareForUpdateStateMachineType` (§10.3.7), `InstallationStateMachineType` (§10.3.8), `PowerCycleStateMachineType` (§10.3.9), `ConfirmationStateMachineType` (§10.3.10) — generated proxies driven by the application. +- Generated proxies for `PrepareForUpdateStateMachineType` (§10.3.7), + `InstallationStateMachineType` (§10.3.8), + `PowerCycleStateMachineType` (§10.3.9), and + `ConfirmationStateMachineType` (§10.3.10), driven by the application. - Storage abstraction: `ISoftwarePackageStore` with `MemoryPackageStore` and `FileSystemPackageStore` implementations. ### Support info & lifetime indication @@ -1085,11 +1030,21 @@ Custom groups go through `WithFunctionalGroup(qualifiedName, action)`. ### DataTypes & VariableTypes -All Device Integration DataTypes (`DeviceHealthEnumeration`, `SoftwareClass`, `LocationIndicationType`, `SoftwareVersionFileType`, `UpdateBehavior`, `FetchResultDataType`, `TransferResultErrorDataType`, `TransferResultDataDataType`, `ParameterResultDataType`) ship as source-generated types in the `Opc.Ua.Di` model library. +The `Opc.Ua.Di` model library includes these source-generated Device +Integration DataTypes: + +- `DeviceHealthEnumeration` +- `SoftwareClass` +- `LocationIndicationType` +- `SoftwareVersionFileType` +- `UpdateBehavior` +- `FetchResultDataType` +- `TransferResultErrorDataType` +- `TransferResultDataDataType` +- `ParameterResultDataType` ### Not yet implemented -- `SoftwareFolderType` (§10.3.5) — multi-version repository. - `TransferServicesType` (§10.4) — parameter set transfer. ## See also diff --git a/docs/Diagnostics.md b/docs/Diagnostics.md index 34074730d7..ba07cbfa17 100644 --- a/docs/Diagnostics.md +++ b/docs/Diagnostics.md @@ -64,7 +64,7 @@ One abstraction covers all three telemetry pillars (logs, traces, metrics). Lifetime semantics are explicit: callers dispose meters they create; the context owns the long-lived `ActivitySource` and `LoggerFactory`. Multiple telemetry contexts may coexist in the same -process — e.g. a server and a client, or several servers with +process — e.g. a server and a client, or several servers with different logging configurations inside a container. ### Extension methods @@ -97,7 +97,7 @@ reports a debug check so missing telemetry is caught early. Telemetry context should be passed **via constructors only**. This enables dependency injection, testability, and `readonly` fields. It -also aligns with the lifecycle of the owning class — for example +also aligns with the lifecycle of the owning class — for example `Dispose` on the class can dispose the meter it created. Existing code can obtain an `ITelemetryContext` from @@ -111,7 +111,7 @@ use this priority order: 0. From a private `ITelemetryContext` member (e.g. `m_telemetry`) of the current class. If the constructor receives any of the types below, pick the first and assign it to a `private readonly` - `m_telemetry` — then use it everywhere in the class. Create + `m_telemetry` — then use it everywhere in the class. Create a `m_logger` field at the same time when one is needed. 1. `ISystemContext.Telemetry` 2. `IServerInternal.Telemetry` @@ -121,7 +121,7 @@ In pure client code: 0. From a private `ITelemetryContext` member, initialized in the constructor from any context in this list. -1. `ISession` → `session.MessageContext.Telemetry` +1. `ISession` → `session.MessageContext.Telemetry` 2. `ISystemContext.Telemetry` 3. `IServiceMessageContext.Telemetry` @@ -132,7 +132,7 @@ so it also receives the context). `Initialize` is called after the If you need to create a context yourself, use the `DefaultTelemetry.Create(...)` static factory. Do **not** create new -contexts in code that already has access to one — plumb the +contexts in code that already has access to one — plumb the existing one through. The application root (typically where you construct `ApplicationInstance` or `HostApplicationBuilder`) is the right place to materialize the context. @@ -196,7 +196,7 @@ pattern end-to-end. This is the pattern we follow for certificate handling, storage, and configuration management. - A logger/meter created in the outer class can be passed to inner - classes created inside the outer class — for example + classes created inside the outer class — for example PubSub message objects. - If a class genuinely cannot obtain a logger yet (gradual migration), initialize the field with `Telemetry.NullLogger.Instance`. In @@ -272,7 +272,7 @@ as a singleton `ITelemetryContext` via `TryAddSingleton`. That adapter resolves the host's `ILoggerFactory` from DI on first use (falling back to `NullLoggerFactory` when none is registered) and materializes a fresh `Meter` / `ActivitySource` per calling -assembly — no further wiring needed for the common case. +assembly — no further wiring needed for the common case. The fluent `.AddLogging(...)` / `.AddMetrics(...)` overloads on `IOpcUaBuilder` are thin pass-throughs to the standard @@ -335,7 +335,7 @@ For Console / Jaeger / Application Insights, swap the | Counter / instrument name | `opc.ua..` lower-snake | `opc.ua.client.connects`, `opc.ua.server.subscriptions.active` | | Tags / dimensions | Stable subset only | `endpoint.url`, `security.mode`, `security.policy.uri` | -Keep tag cardinality bounded — do not tag on session id or any +Keep tag cardinality bounded — do not tag on session id or any per-request identifier. #### Testing patterns @@ -369,7 +369,7 @@ needing a full OpenTelemetry pipeline. The stack creates one `Meter` per assembly that records measurements. The meter's **name is the assembly name** of the component that -created it (via `ITelemetryContext.CreateMeter()` → +created it (via `ITelemetryContext.CreateMeter()` → `Assembly.GetCallingAssembly().FullName`). Most tooling matches with wildcards, so subscribe with `AddMeter("Opc.Ua.Core*", "Opc.Ua.Client*")` to pick up everything the stack emits today. @@ -379,50 +379,50 @@ are documented next to the tag key. #### Meter `Opc.Ua.Core` -Client transport channel manager — defined in +Client transport channel manager — defined in `ClientChannelManagerMetrics.cs`: | Instrument | Kind | Unit | Tags | Description | |---|---|---|---|---| -| `opc.ua.channel.open` | Counter<long> | — | `endpoint`, `reverse` (bool) | OPC UA client transport channels opened. | -| `opc.ua.channel.close` | Counter<long> | — | `endpoint`, `reverse`, `reason` (`lease-released` \| `manager-disposed` \| `faulted`) | OPC UA client transport channels closed. | -| `opc.ua.channel.active` | UpDownCounter<long> | — | `endpoint` | Current number of active OPC UA client channel entries. | -| `opc.ua.channel.reconnect.attempts` | Counter<long> | — | `endpoint`, `outcome` | OPC UA client channel reconnect attempts. | -| `opc.ua.channel.reconnect.duration` | Histogram<double> | `ms` | `endpoint`, `outcome` | Duration of reconnect cycles. | -| `opc.ua.channel.gate.wait` | Histogram<double> | `ms` | `endpoint` | Time spent waiting for the per-channel ready gate. | -| `opc.ua.channel.participant.timeout.count` | Counter<long> | — | `endpoint`, `participant` (kind prefix, e.g. `Session`, `Discovery`) | Reconnect participant callbacks that timed out. | -| `opc.ua.channel.participant.recreate.count` | Counter<long> | — | `endpoint`, `participant`, `success` (bool) | Reconnect participant recreate callbacks. | -| `opc.ua.channel.refcount` | ObservableGauge<long> | — | `endpoint` | Reference count per channel entry. | -| `opc.ua.channel.participants` | ObservableGauge<long> | — | `endpoint` | Participant count per channel entry. | +| `opc.ua.channel.open` | `Counter` | — | `endpoint`, `reverse` (bool) | OPC UA client transport channels opened. | +| `opc.ua.channel.close` | `Counter` | — | `endpoint`, `reverse`, `reason` (`lease-released` \| `manager-disposed` \| `faulted`) | OPC UA client transport channels closed. | +| `opc.ua.channel.active` | `UpDownCounter` | — | `endpoint` | Current number of active OPC UA client channel entries. | +| `opc.ua.channel.reconnect.attempts` | `Counter` | — | `endpoint`, `outcome` | OPC UA client channel reconnect attempts. | +| `opc.ua.channel.reconnect.duration` | `Histogram` | `ms` | `endpoint`, `outcome` | Duration of reconnect cycles. | +| `opc.ua.channel.gate.wait` | `Histogram` | `ms` | `endpoint` | Time spent waiting for the per-channel ready gate. | +| `opc.ua.channel.participant.timeout.count` | `Counter` | — | `endpoint`, `participant` (kind prefix, e.g. `Session`, `Discovery`) | Reconnect participant callbacks that timed out. | +| `opc.ua.channel.participant.recreate.count` | `Counter` | — | `endpoint`, `participant`, `success` (bool) | Reconnect participant recreate callbacks. | +| `opc.ua.channel.refcount` | `ObservableGauge` | — | `endpoint` | Reference count per channel entry. | +| `opc.ua.channel.participants` | `ObservableGauge` | — | `endpoint` | Participant count per channel entry. | Note: the `participant` tag carries the **kind prefix only** (`Session`, `Discovery`, etc.). The per-instance participant id is deliberately omitted to keep metric cardinality bounded; the full id is available on the related Activity tags and on the structured logs emitted under the `Opc.Ua.ChannelManager` logger category for correlation (see [Sessions.md](Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry)). -Client request duration — defined in `ClientBase.cs`: +Client request duration — defined in `ClientBase.cs`: | Instrument | Kind | Unit | Tags | Description | |---|---|---|---|---| -| `opc.ua.client.request.duration` | Histogram<double> | `s` | `opc.ua.request.service` (service name), `opc.ua.response.status.code` (uint), `server.address` (endpoint URL), `opc.ua.request.timeout` (ms) | Wall-clock duration of each client service request. Default bucket boundaries: 5 ms - 60 s. Only emitted when the client's `ActivityTraceFlags` include `ClientTraceFlags.Metrics`. | +| `opc.ua.client.request.duration` | `Histogram` | `s` | `opc.ua.request.service` (service name), `opc.ua.response.status.code` (uint), `server.address` (endpoint URL), `opc.ua.request.timeout` (ms) | Wall-clock duration of each client service request. Default bucket boundaries: 5 ms - 60 s. Only emitted when the client's `ActivityTraceFlags` include `ClientTraceFlags.Metrics`. | -Certificate cache — defined in `CertificateCache.cs`: +Certificate cache — defined in `CertificateCache.cs`: | Instrument | Kind | Unit | Tags | Description | |---|---|---|---|---| -| `opc.ua.certcache.hit` | ObservableCounter<long> | — | — | Total certificate cache hits (public + private key caches combined). | -| `opc.ua.certcache.miss` | ObservableCounter<long> | — | — | Total certificate cache misses. | -| `opc.ua.certcache.size` | ObservableGauge<long> | — | — | Current number of cached certificate entries (both caches). | -| `opc.ua.certcache.private_key_entries` | ObservableGauge<long> | — | — | Current number of cached entries that hold private keys. | -| `opc.ua.certcache.eviction` | ObservableCounter<long> | — | — | Total certificate cache evictions. | +| `opc.ua.certcache.hit` | `ObservableCounter` | — | — | Total certificate cache hits (public + private key caches combined). | +| `opc.ua.certcache.miss` | `ObservableCounter` | — | — | Total certificate cache misses. | +| `opc.ua.certcache.size` | `ObservableGauge` | — | — | Current number of cached certificate entries (both caches). | +| `opc.ua.certcache.private_key_entries` | `ObservableGauge` | — | — | Current number of cached entries that hold private keys. | +| `opc.ua.certcache.eviction` | `ObservableCounter` | — | — | Total certificate cache evictions. | #### Meter `Opc.Ua.Client` -Client node cache — defined in `NodeCache.cs`: +Client node cache — defined in `NodeCache.cs`: | Instrument | Kind | Unit | Tags | Description | |---|---|---|---|---| -| `opc.ua.client.nodecache.hits` | ObservableCounter<long> | — | `cache` (`nodes` \| `values` \| `references`) | Hits on the named sub-cache. | -| `opc.ua.client.nodecache.misses` | ObservableCounter<long> | — | `cache` | Misses on the named sub-cache. | -| `opc.ua.client.nodecache.evictions` | ObservableCounter<long> | — | `cache` | Evictions from the named sub-cache. | -| `opc.ua.client.nodecache.size` | ObservableGauge<long> | — | `cache` | Current entry count of the named sub-cache. | +| `opc.ua.client.nodecache.hits` | `ObservableCounter` | — | `cache` (`nodes` \| `values` \| `references`) | Hits on the named sub-cache. | +| `opc.ua.client.nodecache.misses` | `ObservableCounter` | — | `cache` | Misses on the named sub-cache. | +| `opc.ua.client.nodecache.evictions` | `ObservableCounter` | — | `cache` | Evictions from the named sub-cache. | +| `opc.ua.client.nodecache.size` | `ObservableGauge` | — | `cache` | Current entry count of the named sub-cache. | ### Wiring up a metrics exporter @@ -463,7 +463,7 @@ per OPC UA instrument; tag keys map directly to Prometheus labels. The `.` separators in instrument names become `_` per Prometheus naming rules (e.g. `opc_ua_client_request_duration`). -#### OTLP (OpenTelemetry Protocol → collector / vendor backend) +#### OTLP (OpenTelemetry Protocol → collector / vendor backend) ```csharp using OpenTelemetry.Metrics; @@ -512,7 +512,7 @@ sealed class NullTelemetry : TelemetryContextBase ``` Passing literal `null` for an `ITelemetryContext` parameter is **not -recommended** — it triggers the legacy trace fallback and can +recommended** — it triggers the legacy trace fallback and can emit format exceptions when a non-semantic trace logger receives a semantic logging template. @@ -552,9 +552,11 @@ every event type the standard server raises. ### Enabling auditing Auditing is controlled by the `ServerConfiguration.AuditingEnabled` -flag (and the `OpcUaServerOptions.AuditingEnabled` shortcut when -using the hosted-service builder). When `false`, audit helpers -short-circuit immediately so there is no runtime cost. +flag, which is off by default. A hosted server enables it through the +configuration builder: +`OpcUaServerOptions.ConfigureBuilder = builder => builder.SetAuditingEnabled(true)`. +When `false`, audit helpers short-circuit immediately so there is no +runtime cost. `StandardServer` exposes the active state through `IServerInternal.Auditing` (via `IAuditEventServer.Auditing`). Custom @@ -563,7 +565,7 @@ short-circuit immediately so there is no runtime cost. ### Raising an audit event The `AuditEvents` static class exposes one `Report` method -per spec event — for example `ReportAuditCreateSessionEvent`, +per spec event — for example `ReportAuditCreateSessionEvent`, `ReportAuditOpenSecureChannelEvent`, `ReportAuditCertificateEvent`, `ReportAuditAddNodesEvent`, `ReportAuditHistoryValueUpdateEvent`, `ReportAuditRoleMappingRuleChangedEvent`. Each helper: @@ -630,7 +632,7 @@ automatically through `src/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs`. The runtime cost of maintaining the diagnostics tree (per-session, per-subscription counters) is gated by `ServerConfiguration.DiagnosticsEnabled` (default **true** in `OpcUaServerOptions`). Toggle it at runtime -through the `Server.ServerDiagnostics.EnabledFlag` variable — +through the `Server.ServerDiagnostics.EnabledFlag` variable — writes flow through `DiagnosticsNodeManager.SetDiagnosticsEnabledAsync`, which atomically pauses or resumes the periodic scan loop. @@ -673,7 +675,7 @@ UaExpert relies on. - Subscription diagnostics aggregate across the publishing thread; reading them while the server is under heavy load briefly stalls publishing. -- One scan timer runs at a fixed 1 s interval while diagnostics +- One scan timer runs at a fixed 1 s interval while diagnostics are enabled and at least one diagnostic item is in Sampling or Reporting mode. Ordinary monitored items do not keep this timer alive. Disabling or deleting the last active diagnostic item stops it. @@ -698,7 +700,7 @@ traffic can be decrypted offline. **Do not enable in production.** Operators who enable any part of it should treat every output file as a secret. -#### Defaults — opt-in everything +#### Defaults — opt-in everything | Capability | Default | Opt-in mechanism | |---|---|---| @@ -784,9 +786,9 @@ lifetime has no effect (matches the existing The keylog file extension selects the writer format: -- `*.txt` → NSS-style space-delimited hex tokens +- `*.txt` → NSS-style space-delimited hex tokens (`UaKeyLogTextWriter`). -- everything else (including `*.json` and `*.uakeys.json`) → +- everything else (including `*.json` and `*.uakeys.json`) → JSON-lines (`UaKeyLogJsonWriter`). Whitespace-only values are treated as unset, so accidentally exporting @@ -865,7 +867,7 @@ every security-relevant operation emits a structured event: - `StartCapture`, `StopCapture` - `DumpKeys`, `DecodePcapWithKeys` - `StartReplay`, `StopReplay` -- `FrameCaptured` — rate-limited to 1/min per channel +- `FrameCaptured` — rate-limited to 1/min per channel For tamper-evident storage register the optional `HashChainedAuditFileSink` (writes a JSONL ledger with per-line HMACs). @@ -960,8 +962,8 @@ graph TD introduced in issue [#3288](https://github.com/OPCFoundation/UA-.NETStandard/issues/3288) via the global `TransportBindings.Channels` registry: `AddPcap` installs a `PcapTransportChannelBinding` decorator over the TCP -channel factory, and `ClientChannelManager` — which by default -reads from that same global registry — picks the wrapped +channel factory, and `ClientChannelManager` — which by default +reads from that same global registry — picks the wrapped factory up automatically. There is no extra wiring code; the composition is pure layering at the transport binding level. @@ -977,7 +979,7 @@ free: - **Transparent reconnect.** During the manager's coalesced reconnect cycle the wrapping socket is re-created against the new transport but the `IChannelCaptureRegistry` keeps recording into the same - session file — the reconnect transition appears in the + session file — the reconnect transition appears in the timeline as a state edge rather than a capture interruption. - **Faulted-entry swap.** The `Phase E` `SwapFaultedEntryAsync` path produces a fresh `ChannelEntry` under the same `ManagedChannelKey`; @@ -1001,7 +1003,7 @@ synchronously into the process-wide registry), but the order above reads top-down as "register the channel manager, then your services, then opt in to capture". For non-DI consumers, call `PcapBindings.Install()` / `InstallClient()` / `InstallServer()` at -startup to achieve the same effect — see +startup to achieve the same effect — see [Enabling pcap capture without dependency injection](#enabling-pcap-capture-without-dependency-injection). ### Capture sources @@ -1446,7 +1448,7 @@ The `OPCFoundation.NetStandard.Opc.Ua.PubSub.Diagnostics` package is the PubSub [§4 Packet capture, dissection, and replay](#4-packet-capture-dissection-and-replay). It captures the raw NetworkMessages exchanged over the UDP datagram and MQTT broker transports, writes them to `.pcap` / `.pcapng` for Wireshark, and dissects them -back into structured DataSets — including **decryption of encrypted UADP +back into structured DataSets — including **decryption of encrypted UADP messages** when the matching security keys are available. Targets `net8.0`, `net9.0`, `net10.0`. @@ -1454,14 +1456,14 @@ PubSub is connectionless and message-secured, so it uses its own frame and key-material abstractions rather than the UA-SC channel/token model, but reuses the [§4](#4-packet-capture-dissection-and-replay) `.pcap` / `.pcapng` writers. -### Architecture — capturing transport decorator +### Architecture — capturing transport decorator Capture is implemented as a **transport decorator**, not as code inside the UDP / MQTT transports. `AddPubSubPcap()` wraps every registered `IPubSubTransportFactory` in a `CapturingPubSubTransportFactory`, whose `CapturingPubSubTransport` decorates the real `IPubSubTransport`: on send and receive it taps the raw payload to the active observer and delegates everything -else. This mirrors the UA-SC `CapturingMessageSocket` decorator — the UDP +else. This mirrors the UA-SC `CapturingMessageSocket` decorator — the UDP and MQTT transports contain **no** capture code, and the decorator is inserted only when the diagnostics package is configured via DI. When no capture session has installed an observer, the tap is a single volatile read. @@ -1507,8 +1509,8 @@ Encrypted UADP NetworkMessages ([Part 14 §8.3](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/8.3), [Annex A.2.2.5](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/A.2.2.5)) carry a `SecurityTokenId` in the UADP SecurityHeader. The dissector decrypts a -secured frame when a key resolves for that token id — reusing the -production `UadpSecurityWrapper` — then dissects the recovered cleartext. +secured frame when a key resolves for that token id — reusing the +production `UadpSecurityWrapper` — then dissects the recovered cleartext. Keys come from an `IPubSubKeyResolver`: `CapturedKeyLogKeyResolver` (a captured key log via `PubSubKeyLogReader`, or the keys buffered during capture) or `SksKeyResolver` (a live `IPubSubSecurityKeyProvider`, e.g. a pull provider @@ -1528,7 +1530,7 @@ on host shutdown: ### MCP tools -The reference MCP server exposes the PubSub surface as tools — Action / +The reference MCP server exposes the PubSub surface as tools — Action / configuration, Security Key Service, in-process publish/subscribe runtime, and capture / dissection. See [McpServer.md](McpServer.md#pubsub-tools) for the full catalogue. @@ -1544,21 +1546,21 @@ unchanged. ## 6. Related references -- [Dependency Injection](DependencyInjection.md) — how the +- [Dependency Injection](DependencyInjection.md) — how the stack composes its services, including telemetry, around `IServiceCollection`. -- [Sessions](Sessions.md) — the central +- [Sessions](Sessions.md) — the central `IClientChannelManager` that packet capture decorates. -- [MCP Server](McpServer.md) — surfaces the packet-capture +- [MCP Server](McpServer.md) — surfaces the packet-capture tools (`start_capture`, `dump_keys`, `decode_pcap_with_keys`, `replay_pcap`) over JSON-RPC for LLM-driven workflows. -- [Migration Guide](MigrationGuide.md) — previous releases used +- [Migration Guide](MigrationGuide.md) — previous releases used static `Utils.SetLogger` / `Utils.Trace*` instead of `ITelemetryContext`. The migration appendix lists every breaking change required to adopt the telemetry context. -- [Certificate Manager](CertificateManager.md) — certificate +- [Certificate Manager](CertificateManager.md) — certificate factories and stores all take `ITelemetryContext` on construction. -- [CryptoProvider](CryptoProvider.md) — the `opc.ua.crypto.*` +- [CryptoProvider](CryptoProvider.md) — the `opc.ua.crypto.*` metrics and the audit events that record which cryptographic module performed an operation, including a warning when it carries no validation. diff --git a/docs/DurableSubscription.md b/docs/DurableSubscription.md index 754bc232d9..3b7f3ba1b6 100644 --- a/docs/DurableSubscription.md +++ b/docs/DurableSubscription.md @@ -2,29 +2,56 @@ ## Overview -The durable subscriptions service enables long lifetimes of subscriptions with big queue sizes and therefore zero-dataloss scenarios. -If the connection to the client fails the server continues to sample values. On reconnect the client can use the transfer subscription service to gain access to the missed values. +Durable subscriptions let servers retain subscriptions for longer periods +and use large notification queues. If a client disconnects, the server +continues sampling values. After reconnecting, the client can use the +TransferSubscriptions service to retrieve missed notifications. + +## Contents + +- [Overview](#overview) +- [Fix an existing server with minimal changes](#fix-an-existing-server-with-minimal-changes) +- [Enabling durable subscriptions on an existing server](#enabling-durable-subscriptions-on-an-existing-server) +- [Known limitations and issues](#known-limitations-and-issues) ## Fix an existing server with minimal changes -- If a custom implementation of `INodeManager` is used, or the Method `CreateMonitoredItems` is overridden, add the new parameter `createDurable`. As long as durable Subscriptions are not enabled in the configuration this parameter can be ignored. -- Implement`INodeManager.RestoreMonitoredItems`, this method is only called if durable subscriptions are enabled in the server configuration. -- If a custom `IMonitoredItem` implementation is used, set `IsDurable` to false. Implement a `Dispose` method. +- If you implement a custom `INodeManager` or override + `CreateMonitoredItems`, add the `createDurable` parameter. You can ignore + it while durable subscriptions are disabled in the server configuration. +- Implement `INodeManager.RestoreMonitoredItems`. The server calls this + method only when durable subscriptions are enabled. +- If you implement a custom `IMonitoredItem`, set `IsDurable` to `false` + and implement `Dispose`. ## Enabling durable subscriptions on an existing server Typically the following porting steps are necessary: -- Provide an implementation of `IMonitoredItemQueueFactory` that sets `SupportsDurableQueues` to true. This factory returns your own implementation of `IMonitoredItemQueue` that persists values to storage and supports large queue sizes. As a reference see [DurableMonitoredItemQueueFactory](../../samples/Quickstats.Servers/DurableSubscription/DurableMonitoredItemQueueFactory.cs). -- Register the `IMonitoredItemQueueFactory` by overriding the StandardServer method `CreateMonitoredItemQueueFactory`. -- Provide an implementation of `ISubscriptionStore` that persists and restores subscriptions to storage to continue them after a server restart. The subscription store shall also enshure persistent queues are provided to the monitored items after a restore. -- - Register the `ISubscriptionStore` by overriding the StandardServer method `CreateSubscriptionStore`. -- If a custom implementation of `INodeManager` is used, or the Method `CreateMonitoredItems` is overridden, add the new parameter `createDurable`. Pass the value of this parameter to the MonitoredItem constructor. If a custom queue length check if performed for durable subscriptions check against the durable queue length from SererConfiguration. -- If a custom `IMonitoredItem` implementation is used, implement the `IsDurable` property. Implement a `Dispose` method. Add a `createDurable` paramter to the constructor. -- Make the custom `MonitoredItem` use the `IMonitoredItemQueueFactory` from `IServerInternal.MonitoredItemQueueFactory` to get the registerd durable queues instead of using internal queues for events & value changes. -- To test custom queues use the unit tests in Server Test Project in the file monitoredItemTests and adapt to use your own queue by providing your `IMonitoredItemQueueFactory` in the constructor. +- Implement `IMonitoredItemQueueFactory` and set + `SupportsDurableQueues` to `true`. Return an `IMonitoredItemQueue` that + persists values and supports large queue sizes. See the sample + [DurableMonitoredItemQueueFactory](../samples/Quickstarts.Servers/DurableSubscription/DurableMonitoredItemQueueFactory.cs). +- Register the queue factory by overriding + `StandardServer.CreateMonitoredItemQueueFactory`. +- Implement `ISubscriptionStore` to persist and restore subscriptions across + server restarts. After restoring a subscription, the store must also provide + persistent queues to its monitored items. Register the store by overriding + `StandardServer.CreateSubscriptionStore`. +- If you implement a custom `INodeManager` or override + `CreateMonitoredItems`, add the `createDurable` parameter and pass it to + the `MonitoredItem` constructor. When you check queue length for a durable + subscription, compare it with the durable queue limit in + `ServerConfiguration`. +- If you implement a custom `IMonitoredItem`, add the `createDurable` + constructor parameter, implement `IsDurable`, and implement `Dispose`. +- In a custom `MonitoredItem`, use + `IServerInternal.MonitoredItemQueueFactory` to get the registered durable + queues for events and value changes instead of using internal queues. +- To test custom queues, adapt the monitored-item tests in the server test + project. Provide your `IMonitoredItemQueueFactory` in the test constructor. -Extend the ServerConfiguration +Configure durable subscriptions in `ServerConfiguration`: - Set `DurableSubscriptionsEnabled` to true - Set `MaxDurableNotificationQueueSize` to the desired value @@ -33,7 +60,20 @@ Extend the ServerConfiguration ## Known limitations and issues -- The Quickstarts queues return `false` from `Dequeue` while a batch is still being persisted or cannot be restored within the bounded wait. The queued item count is retained for a later attempt; completion of a failed restore does not mean that the batch data is resident. Restored batch files are deleted after their readers are closed. -- Subscriptions are only persistet on a gracefuls shutdown. If the server crashes or needs to be shut down forcefully all `Subscriptions` / `MonitoredItems` are lost. -- The Quickstarts durable-subscription store writes format version 3. Version 1 records remain readable with publishing enabled and no persisted owner application URI; the retired version 2 is rejected. Current records persist publishing state and the owning client ApplicationUri so an anonymous durable subscription can be transferred after restart. User-name passwords are removed before persistence, and issued-token subscriptions are not persisted because their bearer token is the identity. -- **Breaking change**: The Interfaces for INodeManager & IMonitoredItem were extended to support durable subscriptions. +- The Quickstarts queues return `false` from `Dequeue` while a batch is + being persisted or cannot be restored within the bounded wait. The queue + retains the item count for a later attempt. A failed restore does not mean + the batch data is resident. The store deletes restored batch files after + their readers close. +- The server persists subscriptions only during a graceful shutdown. A crash + or forced shutdown loses all subscriptions and monitored items. +- The Quickstarts durable-subscription store writes format version 3. It can + read version 1 records, which enable publishing but do not persist the + owning application URI. The store rejects retired version 2 records. + Current records persist publishing state and the owning client + `ApplicationUri`, allowing anonymous durable subscriptions to transfer + after restart. The store removes user-name passwords before persistence. + It does not persist issued-token subscriptions because their bearer token + is their identity. +- **Breaking change:** The `INodeManager` and `IMonitoredItem` interfaces + were extended to support durable subscriptions. diff --git a/docs/EccProfiles.md b/docs/EccProfiles.md index 29fd46a8fa..904ccf8c4c 100644 --- a/docs/EccProfiles.md +++ b/docs/EccProfiles.md @@ -1,15 +1,24 @@ # Support for Elliptic Curve Cryptography (ECC) Certificates in Server and Client Applications -The server and client applications now support encrypted communication using both the RSA and the ECC encryption algorithms. -The following document tries to explain the changes in the configuration of the server and client applications needed to support ECC certificates as well as the well consacrated RSA certificates. +Server and client applications support encrypted communication with RSA +and elliptic curve cryptography (ECC) certificates. This guide explains +how to configure both certificate types. -The means by which client and server application security related configuration has been configured upto introducing the ECC certificates, is described in the section [Previous Client and Server application configuration supports only RSA certificates](###previous-client-and-server-application-configuration-supports-only-rsa-certificates). +## Contents -The new security related configuration of client and server applications is described in the section [New Client and Server application configuration support both RSA and ECC certificates](###new-client-and-server-application-configuration-support-both-rsa-and-ecc-certificates). +- [Previous client and server configuration (RSA only)](#previous-client-and-server-application-configuration-supports-only-rsa-certificates) +- [Previous server configuration (RSA only)](#previous-server-application-configuration-supports-only-rsa-certificates) +- [New client and server configuration (RSA and ECC)](#new-client-and-server-application-configuration-support-both-rsa-and-ecc-certificates) +- [New server configuration for ECC](#new-server-application-configuration-related-to-ecc-certificates) +- [Old and new configuration compatibility](#old-client-and-server-application-configuration-format-vs-new-client-and-server-application-configuration-format) +- [Configure GDS for ECC](#configure-gds-for-use-with-ecc-certificates) +- [Known limitations](#known-limitations) -The compatibility between the old and the new configuration of client and server applications is described in the section [Old Client and Server application configuration VS New Client and Server spplication configuration](###old-client-and-server-application-configuration-vs-new-client-and-server-spplication-configuration). - -The limitations of the support for ECC certificates are described in the section [Known Limitations](###known-limitations). +The [previous configuration](#previous-client-and-server-application-configuration-supports-only-rsa-certificates) +supports RSA certificates. The +[new configuration](#new-client-and-server-application-configuration-support-both-rsa-and-ecc-certificates) +supports both RSA and ECC certificates. The guide also describes +compatibility and ECC limitations. ## Previous Client and Server application configuration supports only RSA certificates @@ -187,9 +196,16 @@ With the introduction of ECC certificates, the `` section of t - `http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP256r1` - `http://opcfoundation.org/UA/SecurityPolicy#ECC_brainpoolP384r1` -If ECC specific SecurityPolicies are specified in the `` section of the configuration file, then the server will need to support the corresponding ECC certificates configured in the `` section of the configuration file with the corresponding ``s, not having them configured will result in the server not being able to start. +For every ECC-specific policy in ``, the server must have +a corresponding ECC certificate in ``. Its +`CertificateTypeString` must match the policy's curve. If a corresponding +certificate is missing, the server cannot start. -Since the UserIdentityToken is also encrypted using the RSA or ECC certificate depending on the active security policy or the specified `` of the ``, servers which intend to explicitly state which ECC encryption (if any) of the UserIdentity tokens is supported, should specify the supported `` in the `` section of the configuration file (as it has always been). The supported ``s are the same as the ones specified in the `` section of the configuration file. +The server encrypts each `UserIdentityToken` with the certificate selected +by the active security policy or by the `` in its +``. To explicitly list the policies supported for user +identity tokens, configure them in ``. They must also +appear in ``. The following example shows how the server can specify the supported ``s for the UserIdentityTokens: @@ -218,24 +234,37 @@ The following example shows how the server can specify the supported ` ``` -The `` tag can be ommited in the `` tag, in which case the active security policy is used for the encryption of the UserIdentityToken. That implies that there is a dependency between the `` and the `` sections of the configuration file, meaning that the ``s specified in the `` section should be a subset of the ``s specified in the `` section of the configuration file. +You can omit `` from ``. In that case, +the server uses the active security policy to encrypt the +`UserIdentityToken`. Therefore, each policy listed in `` +must also appear in ``. -A situation in which a `` is specified in the `` section of the configuration file, but it is not specified in the `` section of the configuration file is logically incorrect and produces an invalid configuration. +If `` names a policy that `` does not +include, the configuration is invalid. ## "Old" Client and Server application configuration format VS "New" Client and Server application configuration format -Client and server applications which use the "old" configuration, will continue to work as before, meaning that the server and client certificates will be RSA certificates and the `` tag will be used to configure the RSA certificate. Server applications will have no ECC security policies specified in the `` section of the configuration file. -You should be aware that such applications will not be able to support ECC certificates therefore they will not be able to communicate with clients and servers which use ECC certificates. +Applications using the old configuration continue to use RSA certificates +configured with ``. The server must not list ECC +policies in ``. These applications cannot communicate with +peers that require ECC certificates. -Client and server applications which use the "new" configuration, are be able to support both RSA and ECC certificates. -Server and Client applications are be able to support both RSA and ECC security policies by using the `` tag to configure the RSA and ECC certificates. The `` section of the configuration file are still used to configure the supported security policies which include the new ECC policies. -Additionally the `` section of the configuration file can be used to configure the supported security policies for the UserIdentityTokens which also include the new ECC policies. +Applications using the new configuration can support both RSA and ECC +certificates. Configure both certificate types with +``, then list the supported policies in +``, including any ECC policies. -Combining the "old" and the "new" configuration formats is not supported. That means that the `` tag cannot be used in the same configuration file with the `` tag. +Use `` to configure the policies used to encrypt user +identity tokens. These policies must also appear in ``. + +Do not combine the old and new configuration formats. A configuration file +cannot contain both `` and +``. ## Configure GDS for use with ECC Certificates -To configure the Global Discovery Server for use with ECC Certificates the configuration needs to be updated. +To use ECC certificates with the Global Discovery Server (GDS), update its +configuration as shown below. ```xml diff --git a/docs/FileSystemClient.md b/docs/FileSystemClient.md index da0c6e44db..0b9d0e1eb7 100644 --- a/docs/FileSystemClient.md +++ b/docs/FileSystemClient.md @@ -23,6 +23,28 @@ to make remote file-system navigation feel like working with a local disk. | `FileSystemClientOptions` | n/a | Tuning knobs: `ChunkSize`, `MaxBufferedReadSize`, `PathCacheSize`, type-subtype filters. | | `TemporaryFileTransferClient` + `UaTemporaryWriteFile` | n/a | Separate surface for Part 5 §C.5 atomic temp-file transfers (`GenerateFileForRead/Write` + `CloseAndCommit`). | +## Contents + +- [Quick reference](#quick-reference) +- [Getting started](#getting-started) + - [Open the standard server file system](#open-the-standard-server-file-system) + - [Open any `FileDirectoryType` instance](#open-any-filedirectorytype-instance) + - [Read a file](#read-a-file) + - [Stream a large file](#stream-a-large-file) + - [Create a directory tree and write a file](#create-a-directory-tree-and-write-a-file) + - [Move and delete](#move-and-delete) +- [Path syntax](#path-syntax) +- [Path resolution and caching](#path-resolution-and-caching) +- [Type classification](#type-classification-filetype-vs-filedirectorytype) +- [File metadata](#file-metadata) +- [Streams](#streams) +- [Error mapping](#error-mapping) +- [Move, copy, and delete semantics](#move-copy-and-delete-semantics) +- [Temporary file transfer](#temporary-file-transfer-part-5-c5) +- [What lives where](#what-lives-where) +- [Server-side file bindings](#server-side-file-bindings) +- [See also](#see-also) + ## Getting started ### Open the standard server file system @@ -142,14 +164,16 @@ properties (`MimeType`, `MaxByteStringLength`, `LastModifiedTime`) are returned as `null` when the server does not expose them (`BadNoMatch`, `BadNodeIdUnknown`, or empty target lists are tolerated). -On the stack's `FileSystemNodeManager`, metadata reads and initial monitored-item -values use the asynchronous provider binding without allocating retained file -handle state or opening a stream. The first `Size` notification contains the -file's current byte count and status, not a stored null placeholder. Reattaching -an item after removing and re-adding its manager, or re-enabling a disabled item, -also reads fresh metadata. These initial reads do not add periodic polling for -external file changes; later notifications still require the node's normal -change-notification mechanism. +In the stack's `FileSystemNodeManager`, metadata reads and initial +monitored-item values use the asynchronous provider binding. These reads do +not allocate retained file-handle state or open a stream. The first `Size` +notification contains the file's current byte count and status, not a stored +null placeholder. + +When the manager reattaches an item or re-enables a disabled item, it also +reads fresh metadata. Initial reads do not add periodic polling for external +file changes. Later notifications still require the node's normal change +notification mechanism. `Writable` / `UserWritable` are advisory: callers should not pre-check them before opening a file. Rely on the server's `Open` response for the @@ -303,16 +327,18 @@ Both lazy `FileSystemNodeManager` nodes and materialized move, and copy. A NodeId from another namespace, an invalid encoded type, or a component identifier cannot alias a file-system object. -Virtual directory browsing walks one provider cursor in provider order across -Browse/BrowseNext pages, retaining only the current entry and a continuation's -lookahead reference. It does not snapshot the directory or impose the materialized -binder's `MaxEntries` limit. Named-child lookup scans without retaining unrelated -entries and stops at the match. Exhaustion, cancellation, and continuation release -dispose the cursor exactly once. Cancellation applies to the active page; -cancelling an already-completed page does not cancel subsequent pages. -Synchronous continuation release starts observed asynchronous cursor cleanup -without blocking its caller. Provider failures are surfaced, not converted into -an empty directory. +Virtual directory browsing uses one provider cursor and preserves provider +order across Browse/BrowseNext pages. It retains only the current entry and a +continuation's lookahead reference. It does not snapshot the directory or +apply the materialized binder's `MaxEntries` limit. Named-child lookup scans +without retaining unrelated entries and stops when it finds a match. + +The cursor is disposed exactly once on exhaustion, cancellation, or +continuation release. Cancellation affects only the active page; cancelling +an already completed page does not cancel later pages. Synchronous +continuation release starts and observes asynchronous cursor cleanup without +blocking its caller. The system surfaces provider failures instead of +returning an empty directory. Before Delete or MoveOrCopy reaches an arbitrary provider, the lazy host validates the decoded provider path. Separator-only root aliases resolve to @@ -332,10 +358,11 @@ allocations. At EOF, a valid positive-length read returns an empty byte string. Service calls use the asynchronous read handler; legacy direct synchronous delegates remain available. -File opens reserve compatible access before invoking the provider, so a -rejected erase request cannot truncate a file held by another opener. -Failed or cancelled opens release their reservations; streams returned -after session closure or disposal are closed rather than published. +File opens reserve compatible access before invoking the provider. This +prevents a rejected erase request from truncating a file held by another +opener. Failed or cancelled opens release their reservations. If a provider +returns a stream after session closure or disposal, the system closes it +instead of publishing it. `Read` and `Write` enforce the requested open mode even if the provider's stream supports both operations. `SetPosition` clamps positions beyond EOF to the current file length. `Writable` describes the provider/file diff --git a/docs/FirstModel.md b/docs/FirstModel.md new file mode 100644 index 0000000000..6838f1c9bb --- /dev/null +++ b/docs/FirstModel.md @@ -0,0 +1,577 @@ +# Your first information model + +This tutorial continues +[Route B of Getting started](GettingStarted.md#route-b-build-applications-from-nuget-packages). +You add a small information model to the server, give it behavior, and use it +from the client. The model describes a room thermostat: + +- A `ThermostatType` object type with a measured `Temperature`, a writable + `Setpoint`, and a `Boost` method that raises the setpoint. +- A `Thermostat` object of that type in the server's `Objects` folder. + +The server accepts setpoints from 5 to 30 degrees Celsius. The client finds the +thermostat by name, reads and writes its variables, and calls its method. It +also shows how the server reports an operation that it rejects. + +## Contents + +- [Before you begin](#before-you-begin) +- [1. Describe the model](#1-describe-the-model) +- [2. Add the model to the server](#2-add-the-model-to-the-server) + - [Generate the code](#generate-the-code) + - [Implement the behavior](#implement-the-behavior) + - [Register the node manager](#register-the-node-manager) +- [3. Use the model from the client](#3-use-the-model-from-the-client) + - [Share the model](#share-the-model) + - [Replace the client code](#replace-the-client-code) +- [4. Check the result](#4-check-the-result) +- [What the code does](#what-the-code-does) +- [Troubleshooting](#troubleshooting) +- [Next steps](#next-steps) + +## Before you begin + +Complete steps B1 to B4 of +[Getting started](GettingStarted.md#route-b-build-applications-from-nuget-packages). +This tutorial changes the `GettingStartedServer` and `GettingStartedClient` +projects from those steps. The applications keep their certificates and already +trust each other, so you do not repeat the trust steps. + +The tutorial was verified with package version `2.0.0-preview.5`, the version +that Route B installs. + +If you ran only the repository samples, create the Route B projects first. The +[MinimalCalcServer sample](../samples/MinimalApi/MinimalCalcServer) uses the +same server-side pattern as this tutorial. + +## 1. Describe the model + +A model file declares types and instances. At build time, the SDK source +generator reads the file and generates C# code for it. + +In the `GettingStartedServer` folder, create a `Model` folder and in it a file +named `Heating.xml`: + +```xml + + + + + http://example.com/Heating/ + http://opcfoundation.org/UA/ + + + + Raises the setpoint and returns the new setpoint. + + + The number of degrees to add to the setpoint. + + + + + + + + + A thermostat that controls the temperature of a room. + + + The measured temperature in degrees Celsius. + + + The target temperature in degrees Celsius, from 5 to 30. + + + + + + + The living-room thermostat. + + + ua:Organizes + ua:ObjectsFolder + + + + +``` + +| Part | Meaning | +| --- | --- | +| `TargetNamespace` | The namespace URI that identifies the model. When the server starts, it assigns each namespace URI a number, the *namespace index*. Clients look up the index by the URI. | +| `Prefix="Heating"` | The C# namespace of the generated code. | +| `BoostMethodType` | Declares the arguments of the `Boost` method: one `Double` input and one `Double` output. | +| `ThermostatType` | The object type. Every instance has the `Mandatory` children. `AccessLevel` decides whether clients can write a variable. | +| `Thermostat` | An instance of `ThermostatType`. The inverse `Organizes` reference places it in the `Objects` folder, where clients start browsing. | + +The file uses the ModelDesign format, which is compact to write by hand. The +generator also reads NodeSet2 files: the standard exchange format of modeling +tools and companion specifications. See +[Source-generated node managers](NodeManagers.md#source-generated-node-managers). + +> Declare method arguments in a method type such as `BoostMethodType`, and +> reference it with `TypeDefinition`. With version `2.0.0-preview.5`, arguments +> declared on a method inside an object type produce code that does not compile. + +## 2. Add the model to the server + +Stop the server if it is still running from Getting started. + +### Generate the code + +From the `GettingStartedServer` folder, add the source generator package. It +runs at build time and adds no assembly to your application. + +```bash +dotnet add package OPCFoundation.NetStandard.Opc.Ua.SourceGeneration --prerelease +``` + +Open `GettingStartedServer.csproj`, and add the model file inside the +`` element: + +```xml + + + +``` + +### Implement the behavior + +Create `HeatingNodeManager.cs` in the `GettingStartedServer` folder: + +```csharp +using Opc.Ua; +using Opc.Ua.Server.Fluent; + +namespace Heating; + +[NodeManager(NamespaceUri = "http://example.com/Heating/")] +public partial class HeatingNodeManager +{ + private const double MinimumSetpoint = 5.0; + private const double MaximumSetpoint = 30.0; + + private readonly Lock m_lock = new(); + private double m_setpoint = 21.0; + + partial void Configure(IHeatingNodeManagerBuilder builder) + { + builder.Thermostat.Temperature + .PollEvery(TimeSpan.FromSeconds(1), SampleTemperature); + builder.Thermostat.Setpoint + .OnRead(ReadSetpoint) + .OnWrite(WriteSetpoint); + builder.Thermostat.Boost + .OnCall(Boost); + } + + // Simulates a measurement within half a degree of the setpoint. + private double SampleTemperature() + { + return Math.Round(ReadSetpoint() - 0.5 + Random.Shared.NextDouble(), 1); + } + + private double ReadSetpoint() + { + lock (m_lock) + { + return m_setpoint; + } + } + + private void WriteSetpoint(double value) + { + CheckSetpoint(value); + lock (m_lock) + { + m_setpoint = value; + } + } + + private double Boost(double degrees) + { + lock (m_lock) + { + double newSetpoint = m_setpoint + degrees; + CheckSetpoint(newSetpoint); + m_setpoint = newSetpoint; + return newSetpoint; + } + } + + // The client receives the status code of an exception that a callback throws. + private static void CheckSetpoint(double setpoint) + { + if (setpoint is not (>= MinimumSetpoint and <= MaximumSetpoint)) + { + throw new ServiceResultException( + StatusCodes.BadOutOfRange, + $"The setpoint must be from {MinimumSetpoint} to {MaximumSetpoint}."); + } + } +} +``` + +The `[NodeManager]` attribute asks the generator to complete this partial class. +The generated part loads the model's nodes into the server's address space. The +generator also creates a `HeatingNodeManagerFactory` class and an +`IHeatingNodeManagerBuilder` interface. Set `NamespaceUri` to the model's +namespace URI as a string literal. The generated constant for it, +`Heating.Namespaces.Heating`, cannot be used here, because the attribute is read +before that constant is generated. + +In `Configure`, the builder has a property for each node of the model: +`builder.Thermostat.Setpoint` is the `Setpoint` variable of the `Thermostat` +object. A misspelled name is a compile error. The callbacks connect the nodes to +the state that the node manager holds: + +- `PollEvery` samples `Temperature` every second and updates the node when the + value changes. Reads return the latest sample, and subscriptions receive each + change. +- `OnRead` and `OnWrite` handle each read and write of `Setpoint`. A value that + is returned only by an `OnRead` callback does not reach subscriptions; see + [pushing value changes to subscribers](NodeManagers.md#pushing-runtime-value-changes-to-subscribers). +- `OnCall` receives the method's input as a `double` and returns its output. + +To reject an operation, throw a `ServiceResultException` with a Bad status code. +The server returns that status code to the client. The server checks some rules +itself before it calls your callback: it rejects a value of the wrong data type +and a write to the read-only `Temperature`. + +The server runs callbacks for several requests at the same time, and `PollEvery` +runs in the background, so a lock guards the setpoint. + +### Register the node manager + +Replace the contents of `Program.cs`: + +```csharp +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"); + }) + .AddNodeManager(); + +await builder.Build().RunAsync(); +``` + +Only the `AddNodeManager` line is new. It adds the generated factory, which +creates the node manager when the server starts. + +Start the server: + +```bash +dotnet run +``` + +The server is ready when it logs: + +```text +OPC UA server listening at opc.tcp://localhost:62560/GettingStartedServer. +``` + +Leave the server running. + +## 3. Use the model from the client + +### Share the model + +The client generates code from the same model file: constants for the namespace +URI and the browse names, and a typed client for `ThermostatType`. + +In the second terminal, from the `GettingStartedClient` folder, add the source +generator package: + +```bash +dotnet add package OPCFoundation.NetStandard.Opc.Ua.SourceGeneration --prerelease +``` + +Open `GettingStartedClient.csproj`, and add the following inside the `` +element: + +```xml + + true + + + + + +``` + +`ModelSourceGeneratorOmitFluentApi` turns off the server-side helpers, which +need the server package. + +### Replace the client code + +Replace the contents of `Program.cs`: + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Opc.Ua.Client; + +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"); + 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>>(); + ManagedSession session = await connect(ct); + await using (session) + { + // The server assigns the namespace index at run time. Look it up + // from the namespace URI that the model defines. + int namespaceIndex = session.NamespaceUris.GetIndex(Heating.Namespaces.Heating); + if (namespaceIndex < 0) + { + throw new ServiceResultException( + StatusCodes.BadNotFound, + "The server does not expose the Heating model."); + } + var ns = (ushort)namespaceIndex; + Console.WriteLine($"Heating namespace index: {ns}"); + + // Translate three browse paths, starting at the Objects folder, in one request. + TranslateBrowsePathsToNodeIdsResponse translated = + await session.TranslateBrowsePathsToNodeIdsAsync( + null, + [ + PathFromObjects(ns, Heating.BrowseNames.Thermostat), + PathFromObjects(ns, Heating.BrowseNames.Thermostat, + Heating.BrowseNames.Temperature), + PathFromObjects(ns, Heating.BrowseNames.Thermostat, + Heating.BrowseNames.Setpoint), + ], + ct); + NodeId thermostatId = GetTarget(session, translated.Results[0]); + NodeId temperatureId = GetTarget(session, translated.Results[1]); + NodeId setpointId = GetTarget(session, translated.Results[2]); + + // Read both variables in one request. Each value has its own status. + (ArrayOf values, ArrayOf errors) = + await session.ReadValuesAsync([temperatureId, setpointId], ct); + PrintValue("Temperature", values[0], errors[0]); + PrintValue("Setpoint", values[1], errors[1]); + + // Write a valid setpoint, then one outside the range the server accepts. + foreach (double setpoint in new[] { 22.5, 35.0 }) + { + WriteResponse response = await session.WriteAsync( + null, + [ + new WriteValue + { + NodeId = setpointId, + AttributeId = Attributes.Value, + Value = new DataValue(new Variant(setpoint)), + }, + ], + ct); + + // The Write service succeeds as a whole; each write has its own result. + Console.WriteLine($"Write Setpoint {setpoint}: {response.Results[0]}"); + } + + // Call the Boost method through the client generated from the model. + var thermostat = new Heating.ThermostatTypeClient( + session, + thermostatId, + host.Services.GetRequiredService()); + double newSetpoint = await thermostat.BoostAsync(2.0, ct); + Console.WriteLine($"Boost 2: new setpoint {newSetpoint}"); + try + { + await thermostat.BoostAsync(10.0, ct); + } + catch (ServiceResultException ex) + { + Console.WriteLine($"Boost 10: {ex.StatusCode}"); + } + } +} +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 BrowsePath PathFromObjects(ushort ns, params string[] browseNames) +{ + return new BrowsePath + { + StartingNode = ObjectIds.ObjectsFolder, + RelativePath = new RelativePath + { + Elements = browseNames + .Select(name => new RelativePathElement + { + ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, + IncludeSubtypes = true, + TargetName = new QualifiedName(name, ns), + }) + .ToArray(), + }, + }; +} + +static NodeId GetTarget(ManagedSession session, BrowsePathResult result) +{ + if (StatusCode.IsBad(result.StatusCode) || result.Targets.Count == 0) + { + throw new ServiceResultException( + result.StatusCode, + "A browse path was not found."); + } + return ExpandedNodeId.ToNodeId(result.Targets[0].TargetId, session.NamespaceUris); +} + +static void PrintValue(string name, DataValue value, ServiceResult error) +{ + if (ServiceResult.IsBad(error) || StatusCode.IsBad(value.StatusCode)) + { + Console.WriteLine($"{name}: failed with {value.StatusCode}"); + } + else if (value.WrappedValue.TryGetValue(out double number)) + { + Console.WriteLine($"{name}: {number} ({value.StatusCode})"); + } + else + { + Console.WriteLine($"{name}: unexpected value {value.WrappedValue}"); + } +} +``` + +## 4. Check the result + +Run the client: + +```bash +dotnet run +``` + +The client prints output like this and exits with exit code 0: + +```text +Heating namespace index: 2 +Temperature: 21.1 (Good [0x00000000]) +Setpoint: 21 (Good [0x00000000]) +Write Setpoint 22.5: Good [0x00000000] +Write Setpoint 35: BadOutOfRange [0x803C0000] +Boost 2: new setpoint 24.5 +Boost 10: BadOutOfRange [0x803C0000] +``` + +The temperature varies, and the number format follows your system's culture. +The server rejects the setpoint 35 and the boost to 34.5, because both are above +30. + +The server keeps its state between client runs: if you run the client again, the +setpoint starts at 24.5. Restart the server to start again from 21. When you are +done, press Ctrl+C in the server terminal. + +## What the code does + +| Step | Code | Learn more | +| --- | --- | --- | +| Find the namespace | `session.NamespaceUris.GetIndex(Heating.Namespaces.Heating)` returns the index that this server assigned to the model's namespace URI. `Heating.Namespaces.Heating` is a generated constant. Do not hard-code namespace indexes: they can differ between servers. | [Concepts: node identity and namespaces](Concepts.md#node-identity-and-namespaces) | +| Find the nodes | `TranslateBrowsePathsToNodeIdsAsync` follows browse names from the `Objects` folder. It returns the `NodeId` of each path, or a Bad status such as `BadNoMatch`. The server chooses the `NodeId`s of instances; browse paths are the same on every server that implements the model. | [Concepts: the address space](Concepts.md#the-address-space) | +| Read | `ReadValuesAsync` reads several values in one request, and returns a `DataValue` and a `ServiceResult` for each. | [Concepts: values, status codes, and timestamps](Concepts.md#values-status-codes-and-timestamps) | +| Write | `WriteAsync` returns one status code for each value in the request. The request succeeds even when the server rejects a value. The value must have the variable's data type: `22.0` is a `Double`, but `22` is an `Int32`, which fails with `BadTypeMismatch`. | [Concepts: service results and errors](Concepts.md#service-results-and-errors) | +| Call | `ThermostatTypeClient` is generated from `ThermostatType`. Its `BoostAsync` method calls `Boost` with typed arguments, and throws a `ServiceResultException` when the call fails. | [Methods with arguments](NodeManagers.md#methods-with-arguments--typed-oncall-overloads) (server side) | +| Implement nodes | The generated typed builder attaches `PollEvery`, `OnRead`, `OnWrite`, and `OnCall` callbacks to the model's nodes. | [Typed model traversal](NodeManagers.md#typed-model-traversal--the-configureimanagernodemanagerbuilder-partial) | + +## Troubleshooting + +| Symptom | Cause | What to do | +| --- | --- | --- | +| The client build fails with `error CS0234: The type or namespace name 'Server' does not exist in the namespace 'Opc.Ua'`. | The generator creates server-side helpers, which need the server package. | Set `ModelSourceGeneratorOmitFluentApi` to `true` in the client project. | +| The server build fails with `error CS0234: The type or namespace name 'BoostMethodState' does not exist in the namespace 'Heating'`. | With `2.0.0-preview.5`, the model declares the method arguments on the method inside `ThermostatType`. | Declare the arguments in a method type, as in [step 1](#1-describe-the-model). | +| The server build fails with `error MODELGEN035: The NamespaceUri expression ... could not be resolved during source generation`. | `[NodeManager]` uses a generated constant, such as `Namespaces.Heating`. | Use the namespace URI as a string literal. | +| `The server does not expose the Heating model.` | The server does not register the node manager, or the model uses another namespace URI. | Check the `AddNodeManager` call in the server, and the `TargetNamespace` in the model. | +| `OPC UA error BadNoMatch [0x806F0000]: A browse path was not found.` | A browse name in the client does not match the model. Browse names are case-sensitive. | Use the generated `Heating.BrowseNames` constants. | +| A write fails with `BadTypeMismatch`. | The value's data type differs from the variable's data type. | Write a `Double`, such as `22.0`. | +| `BadSecurityChecksFailed` or `BadCertificateUntrusted` | The applications do not trust each other. | Repeat [step B3 of Getting started](GettingStarted.md#b3-trust-each-application). | + +## Next steps + +- Monitor the temperature: pass `temperatureId` to `SubscribeDataChangesAsync`, + as the Getting started client does for the server clock. See + [Subscriptions](Subscriptions.md#streaming-subscriptions). +- Learn more about generated node managers: NodeSet2 models, instances created + at run time, events, and alarms. See + [Source-generated node managers](NodeManagers.md#source-generated-node-managers). +- Load models into a running server without generated code. See + [Runtime NodeSets](RuntimeNodeSets.md#quick-start-examples). +- Define your own structures. See + [Source-generated data types](SourceGeneratedDataTypes.md). +- Use a standard model, such as Device Integration. See + [Use companion models](README.md#4-use-companion-models). +- Before you deploy, work through the + [production readiness checklist](ProductionChecklist.md). diff --git a/docs/GDS.md b/docs/GDS.md index 06ca8c96b7..c0e8e5190c 100644 --- a/docs/GDS.md +++ b/docs/GDS.md @@ -14,6 +14,50 @@ control. | [KeyCredentialService](KeyCredentialService.md) | Credential issuance for non-UA services | | [AuthorizationService](AuthorizationService.md) | OAuth2-style access token issuance | +## Contents + +- [Quick Links](#quick-links) +- [Packages](#packages) +- [1. Client API](#1-client-api) + - [GlobalDiscoveryServerClient](#globaldiscoveryserverclient) + - [ServerPushConfigurationClient](#serverpushconfigurationclient) + - [OPC 10000-21 OnboardingClient](#opc-10000-21-onboardingclient) +- [2. Server-Side: Building a GDS](#2-server-side-building-a-gds) + - [Minimal GDS Server](#minimal-gds-server) + - [Using GdsNodeManagerFactory](#using-gdsnodemanagerfactory) + - [Extension Points](#extension-points) +- [3. Implementing Providers](#3-implementing-providers) + - [IApplicationsDatabase](#iapplicationsdatabase) + - [ICertificateGroup](#icertificategroup) + - [IGdsUserDatabase](#igdsuserdatabase) + - [IConfigurationDataStore](#iconfigurationdatastore) +- [4. Roles and Authorization](#4-roles-and-authorization) + - [GDS Roles (OPC 10000-12 §7.2)](#gds-roles-opc-10000-12-72) + - [ApplicationSelfAdmin Privilege](#applicationselfadmin-privilege) + - [ApplicationAdmin Privilege](#applicationadmin-privilege) + - [Security: Fail-Closed Channel Validation](#security-fail-closed-channel-validation) +- [5. End-to-End Example](#5-end-to-end-example) +- [Conformance Matrix](#conformance-matrix) + - [Applicable Part 12 profiles and conformance units](#applicable-part-12-profiles-and-conformance-units) + - [Automated verification](#automated-verification) +- [GDS Directory (§6.5)](#gds-directory-65) +- [Certificate Management — Pull Model (§7.9)](#certificate-management--pull-model-79) +- [Roles and Privileges (§7.2)](#roles-and-privileges-72) +- [Push Management — ServerConfiguration (§7.10)](#push-management--serverconfiguration-710) + - [Transaction model, diagnostics and lifecycle (§7.10.2, §7.10.9, §7.10.11, §7.10.17)](#transaction-model-diagnostics-and-lifecycle-7102-7109-71011-71017) + - [Optional ServerConfiguration surface (§7.10.13, §7.10.14, §7.10.20)](#optional-serverconfiguration-surface-71013-71014-71020) + - [GDS-managed applications proxy (§7.10.14–§7.10.16)](#gds-managed-applications-proxy-7101471016) +- [TrustList (§7.8)](#trustlist-78) + - [Certificate and TrustList alarms (§7.8.3)](#certificate-and-trustlist-alarms-783) + - [TrustList-change effects on channels and Sessions (§7.10.9)](#trustlist-change-effects-on-channels-and-sessions-7109) +- [Audit Events](#audit-events) +- [KeyCredentialService (§8)](#keycredentialservice-8) +- [AuthorizationService (§9)](#authorizationservice-9) + - [Refresh tokens](#refresh-tokens) +- [Remaining optional / unsupported Part 12 items](#remaining-optional--unsupported-part-12-items) +- [LDS / LDS-ME (§4–5)](#lds--lds-me-45) +- [Extension Points (Phase 2 Abstractions)](#extension-points-phase-2-abstractions) + ## Packages | NuGet Package | Contents | @@ -598,7 +642,13 @@ The GDS-side proxy that exposes *other* applications' configurations under a `Ma ### Certificate and TrustList alarms (§7.8.3) -Each `CertificateGroup` exposes the two optional standard alarm instances with **full active/inactive transitions and events** (previously property-only). `ConfigurationNodeManager` creates and wires them in `CreateAddressSpace` and `StandardServer.OnServerStarted` starts periodic monitoring (60 s) once the subscription infrastructure is ready; all thresholds and timers flow through the injected `TimeProvider`. See [CertificateManager.md → Certificate-Expiration and TrustList-Staleness Alarms](CertificateManager.md#certificate-expiration-and-trustlist-staleness-alarms-opc-ua-part-12-783). +Each `CertificateGroup` exposes the two optional standard alarm instances +with **full active/inactive transitions and events** (previously +property-only). `ConfigurationNodeManager` creates and wires them in +`CreateAddressSpace`. `StandardServer.OnServerStarted` starts periodic +monitoring every 60 s once the subscription infrastructure is ready. The +injected `TimeProvider` supplies all thresholds and timers. See +[CertificateManager.md → Certificate-Expiration and TrustList-Staleness Alarms](CertificateManager.md#certificate-expiration-and-trustlist-staleness-alarms-opc-ua-part-12-783). | Alarm | Status | Source | Tests | |-------|--------|--------|-------| diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 0000000000..dc005e381e --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,493 @@ +# Getting started + +This guide gets an OPC UA client and server talking on one computer. Choose one +of two entry routes: + +- **[Route A: Run the repository samples](#route-a-run-the-repository-samples)** + if you have cloned this repository, or want to explore it. You build and run + the minimal server and client samples from source. +- **[Route B: Build applications from NuGet packages](#route-b-build-applications-from-nuget-packages)** + if you are starting your own project. You create a small server and client + from the published packages. + +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](#what-the-code-does) explains each step. +The [concepts guide](Concepts.md) explains the underlying OPC UA terms. + +## Contents + +- [Before you begin](#before-you-begin) +- [Route A: Run the repository samples](#route-a-run-the-repository-samples) + - [A1. Start the server](#a1-start-the-server) + - [A2. Run the client and trust the server](#a2-run-the-client-and-trust-the-server) + - [A3. Check the result](#a3-check-the-result) + - [A4. Stop and clean up](#a4-stop-and-clean-up) +- [Route B: Build applications from NuGet packages](#route-b-build-applications-from-nuget-packages) + - [B1. Create the server](#b1-create-the-server) + - [B2. Create the client](#b2-create-the-client) + - [B3. Trust each application](#b3-trust-each-application) + - [B4. Check the result](#b4-check-the-result) + - [B5. Stop and clean up](#b5-stop-and-clean-up) + - [Connect to another server](#connect-to-another-server) +- [What the code does](#what-the-code-does) +- [Troubleshooting](#troubleshooting) +- [Next steps](#next-steps) + +## 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`](../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`](../samples/MinimalApi/MinimalBoilerServer) hosts a + small Boiler model. Its node manager is generated from the model at build time. +- [`MinimalClient`](../samples/MinimalApi/MinimalClient/README.md) 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 + +```bash +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: + +```text +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: + +```bash +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: + +```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: + +```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: + +```text +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 + +```bash +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`: + +```csharp +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: + +```bash +dotnet run +``` + +On first start, the server creates its application certificates in `pki`. It +is ready when it logs: + +```text +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: + +```bash +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`: + +```csharp +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>>(); + 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 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: + + ```text + OPC UA error BadSecurityChecksFailed [0x80130000]: ... + ``` + + The server has refused the client certificate. In the + `GettingStartedServer` folder, trust it: + + PowerShell: + + ```powershell + New-Item -ItemType Directory -Force pki/trusted/certs | Out-Null + Move-Item pki/rejected/certs/*.der pki/trusted/certs + ``` + + bash: + + ```bash + mkdir -p pki/trusted/certs + mv pki/rejected/certs/*.der pki/trusted/certs/ + ``` + +2. Run the client again. It now stops with: + + ```text + 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: + +```text +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](#b3-trust-each-application), +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](IdentityProviders.md). 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](Concepts.md#applications), [Certificates](Certificates.md) | +| 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](DependencyInjection.md#server-feature) | +| Select an endpoint | `AddDiscoveryAndConnect` calls `GetEndpoints` on the URL and selects the endpoint with the configured security mode and policy. | [Concepts: endpoints](Concepts.md#endpoints-secure-channels-and-sessions) | +| Connect | The registered `Func>` connects a `ManagedSession`, which reconnects automatically after connection loss. | [Sessions](Sessions.md#3-managedsession--the-connection-state-machine-facade) | +| Browse | `Browser` follows hierarchical references from the `Objects` folder. | [Concepts: address space](Concepts.md#the-address-space) | +| Read | `ReadValueAsync` returns a `DataValue` whose status is Good or Uncertain, and throws a `ServiceResultException` when the status is Bad. | [Concepts: values](Concepts.md#values-status-codes-and-timestamps) | +| 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](Subscriptions.md#streaming-subscriptions) | +| Clean up | `await using` closes the session and its subscription. The timeout bounds the whole run. | [Concepts: SDK organization](Concepts.md#how-the-sdk-is-organized) | + +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](ProductionChecklist.md). + +## 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](Profiles.md#security-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](Certificates.md) for certificate stores and chains, and +[Diagnostics](Diagnostics.md) for logging and packet capture. + +## Next steps + +- Learn the terms used throughout the documentation in + [OPC UA concepts](Concepts.md). +- Add your own model to the Route B server, and read, write, and call it from + the client, in [Your first information model](FirstModel.md). +- Continue on the path for your application in the + [documentation index](README.md#2-build-your-application): + a client, a server with your own model, or a PubSub application. +- Before you deploy, work through the + [production readiness checklist](ProductionChecklist.md): certificate + management, user authentication, resource limits, and diagnostics. diff --git a/docs/HighAvailability.md b/docs/HighAvailability.md index 59bc909ba8..7cd39ba29f 100644 --- a/docs/HighAvailability.md +++ b/docs/HighAvailability.md @@ -1,11 +1,48 @@ # High Availability and OPC UA Redundancy -This guide maps the OPC UA .NET Standard high-availability APIs to OPC 10000-4 §6.6 Redundancy. It documents the implemented server, client, subscription, session, [Kubernetes](Kubernetes.md), and active/active extension seams; the worked examples are `samples/Redundancy/RedundantServer` and `samples/Redundancy/RedundantClient`. +This guide explains how the OPC UA .NET Standard high-availability APIs +implement OPC 10000-4 §6.6 Redundancy. It covers server, client, +subscription, and session redundancy, plus [Kubernetes](Kubernetes.md) +and active/active extensions. Worked examples are in +`samples/Redundancy/RedundantServer` and +`samples/Redundancy/RedundantClient`. -Redundancy and high availability are opt-in and require adding the extra `OPCFoundation.NetStandard.Opc.Ua.Redundancy.*` NuGet packages (for example `OPCFoundation.NetStandard.Opc.Ua.Redundancy.Server` or `.Client`) to your application. A server or client built only with the standard `OPCFoundation.NetStandard.Opc.Ua.Client` and `OPCFoundation.NetStandard.Opc.Ua.Server` libraries does not support OPC UA redundancy. +Redundancy and high availability are opt-in. Add the relevant +`OPCFoundation.NetStandard.Opc.Ua.Redundancy.*` NuGet package to your +application, such as `OPCFoundation.NetStandard.Opc.Ua.Redundancy.Server` +or `.Client`. The standard `OPCFoundation.NetStandard.Opc.Ua.Client` and +`OPCFoundation.NetStandard.Opc.Ua.Server` libraries alone do not provide +OPC UA redundancy. For distributed PubSub active/standby publishers and subscribers, see the PubSub counterpart: [PubSub High Availability](PubSubHighAvailability.md). +## Contents + +- [Redundancy overview (as per Part 4 §6.6.1)](#redundancy-overview-as-per-part-4-661) +- [Server redundancy (as per Part 4 §6.6.2)](#server-redundancy-as-per-part-4-662) + - [Server.ServerRedundancy model](#serverserverredundancy-model) + - [Add* and Use* API convention](#add-and-use-api-convention) +- [ServiceLevel and load balancing (as per Part 4 §6.6.2.4.2 and §6.6.2.4.3)](#servicelevel-and-load-balancing-as-per-part-4-66242-and-66243) +- [Non-transparent failover modes and client actions (as per Part 4 §6.6.2.4.5)](#non-transparent-failover-modes-and-client-actions-as-per-part-4-66245) +- [Manual failover and Maintenance (as per Part 4 §6.6.5)](#manual-failover-and-maintenance-as-per-part-4-665) +- [HotAndMirrored and Transparent state mirroring](#hotandmirrored-and-transparent-state-mirroring) + - [Active/passive address-space consistency](#activepassive-address-space-consistency) + - [Strong active/passive historian](#strong-activepassive-historian) +- [Client redundancy (as per Part 4 §6.6.3)](#client-redundancy-as-per-part-4-663) +- [Network redundancy (as per Part 4 §6.6.4)](#network-redundancy-as-per-part-4-664) +- [Beyond §6.6: distributed extensions](#beyond-66-distributed-extensions) + - [Dynamic peer discovery (beyond §6.6, opt-in)](#dynamic-peer-discovery-beyond-66-opt-in) + - [Sharing values across replicas: distributed value cache (beyond §6.6, opt-in)](#sharing-values-across-replicas-distributed-value-cache-beyond-66-opt-in) + - [Shared certificate stores (distributed trust lists) (beyond §6.6, opt-in)](#shared-certificate-stores-distributed-trust-lists-beyond-66-opt-in) + - [Distributed PushManagement transactions (beyond §6.6, opt-in)](#distributed-pushmanagement-transactions-beyond-66-opt-in) + - [GetEndpoints load direction (beyond §6.6, opt-in)](#getendpoints-load-direction-beyond-66-opt-in) + - [Client-side high availability (replica sets)](#client-side-high-availability-replica-sets) +- [Kubernetes deployment](#kubernetes-deployment) +- [Samples](#samples) +- [Security considerations](#security-considerations) + - [Record context and plaintext ownership](#record-context-and-plaintext-ownership) + - [Shared application identity](#shared-application-identity) + ## Redundancy overview (as per Part 4 §6.6.1) OPC UA defines three independent but composable redundancy dimensions: **server redundancy** gives clients multiple servers that expose the same data, **client redundancy** lets backup clients take over work from an active client, and **network redundancy** gives a client and server more than one communication path. The stack implements standardized discovery and failover metadata in the core server/client packages, and adds opt-in distributed state through `OPCFoundation.NetStandard.Opc.Ua.Redundancy.Server`. @@ -629,7 +666,7 @@ builder }); ``` -Give every replica a distinct `ReplicaId`, keep `RenewInterval` well below `LeaseDuration`, and share the same `KeyPrefix` and record-protection key across the set. See [Certificate Manager — PushManagement Transactions](CertificateManager.md#pushmanagement-transactions-opc-ua-part-12-71027101) for the underlying transaction model. +Give every replica a distinct `ReplicaId`, keep `RenewInterval` well below `LeaseDuration`, and share the same `KeyPrefix` and record-protection key across the set. See [Certificate Manager — PushManagement Transactions](CertificateManager.md#pushmanagement-transactions-opc-ua-part-12-7102-71011) for the underlying transaction model. ### GetEndpoints load direction (beyond §6.6, opt-in) diff --git a/docs/HistoricalAccess.md b/docs/HistoricalAccess.md index 2b416d7561..23e117f814 100644 --- a/docs/HistoricalAccess.md +++ b/docs/HistoricalAccess.md @@ -1,17 +1,23 @@ # Historical Access (OPC UA Part 11) +## Contents + +- [Overview](#overview) +- **[Architecture](#architecture)** and **[Scope](#scope)** — what's implemented and how it fits together. +- **[Server developer guide](#server-developer-guide)** — quick start, fluent builder, the registry, per-NodeManager wiring, annotations, configuration node. +- **[Provider author guide](#provider-author-guide)** — interface-by-interface contract for a custom historian, including resume tokens, pagination, status codes, error semantics, and thread safety. +- **[Client developer guide](#client-developer-guide)** — `HistoryClient` usage patterns. +- [Automatic value capture](#automatic-value-capture) +- **[Capability discovery](#capability-discovery)** and **[Auditing](#auditing)**. +- [Part 11 profile conformance catalog](#part-11-profile-conformance-catalog) +- [Limitations and roadmap](#limitations-and-roadmap) + ## Overview -The .NET Standard stack implements OPC UA Part 11 Historical Access end-to-end via a clean **provider model** that lets you back historizing variables with any time-series storage engine. A reference-quality **in-memory engine** ships in `Opc.Ua.Server` so getting started requires no extra packages. - -This document is split into: - -- **[Architecture](#architecture)** and **[Scope](#scope)** — what's implemented and how it fits together. -- **[Server developer guide](#server-developer-guide)** — quick start, fluent builder, the registry, per-NodeManager wiring, annotations, configuration node. -- **[Provider author guide](#provider-author-guide)** — interface-by-interface contract for writing a custom historian, including resume tokens, pagination, status codes, error semantics, thread-safety. -- **[Client developer guide](#client-developer-guide)** — `HistoryClient` usage patterns. -- **[Capability discovery](#capability-discovery)** and **[Auditing](#auditing)**. -- **[Limitations and roadmap](#limitations-and-roadmap)**. +The .NET Standard stack implements OPC UA Part 11 Historical Access through +a **provider model**. Use it to back historizing variables with any +time-series storage engine. `Opc.Ua.Server` also includes an +**in-memory engine**, so you can get started without extra packages. ## Architecture @@ -419,28 +425,38 @@ Without a resolved historian, the existing aggregate revision behavior is preserved: the processing interval is at least the monitored-item sampling interval and the start time is clamped directly to the retained queue window. -When the revised aggregate `StartTime` is in the past and the provider -implements `IHistorianDataProvider`, the async node-manager path pages raw -history from `StartTime` to one captured server time and queues it -oldest-to-newest before live delivery begins. Live values arriving while the -provider is being read are buffered and delivered after the historical window; -the current value is never queued ahead of that history. Future start times, -non-Value attributes, and nodes without a raw historian retain the normal -current-value initialization. Provider read failures queue an error -notification and leave the monitored item active for later live values rather -than silently falling back. A live-value buffer overflow fails creation because -the history-to-live ordering can no longer be guaranteed. Required priming-error -notifications remain protected while the live queue size is greater than one. -A queue size of one retains only the newest notification, including after a -resize: later samples may replace an error before publication, as specified in +When a revised aggregate `StartTime` is in the past and the provider implements +`IHistorianDataProvider`, the async node-manager path: + +- Reads raw history from `StartTime` to one captured server time. +- Queues the history oldest-to-newest before live delivery begins. +- Buffers live values that arrive during the read, then delivers them after + the historical window. It never queues the current value ahead of history. + +The following cases keep the normal current-value initialization: + +- The start time is in the future. +- The monitored attribute is not `Value`. +- The node has no raw historian. + +If a provider read fails, the server queues an error notification and keeps +the monitored item active for later live values; it does not silently fall +back. If the live-value buffer overflows, creation fails because the server +can no longer guarantee history-to-live ordering. + +The server protects required priming-error notifications while the live queue +size is greater than one. A queue size of one retains only the newest +notification, even after a resize. Later samples can replace an error before +publication, as specified in [Part 4, 5.13.1.5](https://reference.opcfoundation.org/specs/OPC-10000-4/5.13.1.5). -Growing that single-value buffer does not restore protection. -Protection is transient queue state: durable restore retains the monitored-item -definition, last value/error, and stored raw queue values, but does not reinstate priming -protection or synthesize a notification absent from the stored queue. -The sample store uses format 1 and shared-store definitions use format 3; -interim formats that included notification-priority metadata (sample format 2 -and shared format 4) are not supported. +Growing a single-value queue does not restore protection. + +Priming protection is transient queue state. Durable restore retains the +monitored-item definition, last value or error, and stored raw queue values. +It does not restore priming protection or synthesize a notification missing +from the stored queue. The sample store uses format 1; shared-store +definitions use format 3. The system does not support interim formats with +notification-priority metadata (sample format 2 and shared format 4). Modifying an existing monitored item to a past-start aggregate uses the same history-before-live handoff whenever the aggregate calculator changes. diff --git a/docs/IdentityProviders.md b/docs/IdentityProviders.md index 57e82b0f13..fbdc0a0f92 100644 --- a/docs/IdentityProviders.md +++ b/docs/IdentityProviders.md @@ -1,9 +1,9 @@ # Identity Providers (OPC UA Part 6 §6.5) -The OPC UA .NET Standard stack exposes a pluggable identity-provider model -that covers every user identity mechanism defined in -[OPC UA Part 6 §6.5](https://reference.opcfoundation.org/Core/Part6/v105/docs/6.5) and the -identity-provider handshakes described in +The OPC UA .NET Standard stack provides a pluggable identity-provider model. +It covers the user-identity mechanisms in +[OPC UA Part 6 §6.5](https://reference.opcfoundation.org/Core/Part6/v105/docs/6.5) +and the identity-provider handshakes in [OPC UA Part 4 §6.2](https://reference.opcfoundation.org/Core/Part4/v105/docs/6.2). The design is intentionally **symmetric**: @@ -35,6 +35,39 @@ on-the-wire types. The provider model layers on top of those types so legacy callbacks keep working during migration, while new code can register `IUserTokenAuthenticator` instances directly. +## Contents + +- [Quick start — dependency injection](#quick-start--dependency-injection) + - [Server example](#server-example) + - [Custom authenticator registrations](#custom-authenticator-registrations) + - [Client example](#client-example) + - [GDS example](#gds-example) + - [Configuration reference](#configuration-reference) +- [Three layers, kept separate](#three-layers-kept-separate) +- [Client side](#client-side) + - [`IClientIdentityProvider` — what to send in `ActivateSession`](#iclientidentityprovider--what-to-send-in-activatesession) + - [`IAccessTokenProvider` — orthogonal to the OPC UA stack](#iaccesstokenprovider--orthogonal-to-the-opc-ua-stack) + - [`AuthorizationServerMetadata` — the JSON nobody told you about](#authorizationservermetadata--the-json-nobody-told-you-about) +- [Server side](#server-side) + - [`IUserTokenAuthenticator` — what to do with the incoming token](#iusertokenauthenticator--what-to-do-with-the-incoming-token) + - [`IServerIdentityRegistry` — composing authenticators](#iserveridentityregistry--composing-authenticators) + - [Identity augmenters](#identity-augmenters) + - [Claims surface — wiring `IdentityCriteriaType.GroupId` and `Role`](#claims-surface--wiring-identitycriteriatypegroupid-and-role) + - [`ITokenIssuer` — server-side JWT issuance](#itokenissuer--server-side-jwt-issuance) + - [`IIssuerKeyResolver` + `IssuerVerificationKey` — JWT validation](#iissuerkeyresolver--issuerverificationkey--jwt-validation) +- [How-to: server-side authentication](#how-to-server-side-authentication) +- [How-to: client-side provider selection](#how-to-client-side-provider-selection) +- [How-to: migrate from `SessionManager.ImpersonateUser`](#how-to-migrate-from-sessionmanagerimpersonateuser) + - [1. Legacy event code](#1-legacy-event-code) + - [2. Implement an authenticator for that token type](#2-implement-an-authenticator-for-that-token-type) + - [3. Register via dependency injection or the server registry](#3-register-via-dependency-injection-or-the-server-registry) +- [Implementing your own provider](#implementing-your-own-provider) + - [Entra ID provider](#entra-id-provider) + - [OIDC provider](#oidc-provider) + - [Windows Integrated provider](#windows-integrated-provider) + - [ASP.NET Core provider](#aspnet-core-provider) +- [See also](#see-also) + ## Quick start — dependency injection For applications hosted on `Microsoft.Extensions.Hosting` (the default @@ -323,10 +356,10 @@ separate so each layer can be replaced independently: | **Claim extraction** | `IIdentityClaims` (probe interface on the returned identity) | Surfaces OIDC / JWT / X.509 claims so role mapping has data to work with. | Decide which roles get granted. | | **Role mapping** | `IRoleManager.ResolveGrantedRoles` (already exists, see [Role-Based Security](RoleBasedUserManagement.md)) | Applies OPC UA Part 18 §4.4 identity-mapping rules to the claims and emits the granted role NodeIds. | Authenticate the token. | -If you find yourself granting roles inside an authenticator, you have -overstepped — push that logic into an `IRoleManager` identity-mapping -rule instead, then the same rule applies whether the user came in via -UserName, X509, JWT, or a future token type. +Do not grant roles inside an authenticator. Put role assignment in an +`IRoleManager` identity-mapping rule instead. The same rule then applies +whether the user authenticated with a username, X.509 certificate, JWT, or +another token type. ## Client side @@ -510,11 +543,11 @@ public sealed class StaticUserNameAuthenticator : IUserTokenAuthenticator public StaticUserNameAuthenticator(Func checkPassword) { - m_checkPassword = checkPassword; + m_checkPassword = checkPassword ?? throw new ArgumentNullException(nameof(checkPassword)); } public UserTokenType TokenType => UserTokenType.UserName; - public string IssuedTokenProfileUri => null; // n/a for UserName + public string? IssuedTokenProfileUri => null; public ValueTask AuthenticateAsync( AuthenticationContext context, @@ -756,6 +789,8 @@ using Opc.Ua.Identity; using Opc.Ua.Server; using Opc.Ua.Server.Hosting; +services.AddSingleton>(checkPassword); + services.AddOpcUa() .AddServer(o => { @@ -763,7 +798,7 @@ services.AddOpcUa() o.ApplicationUri = "urn:example:my-server"; o.EndpointUrls.Add("opc.tcp://localhost:4840"); }) - .AddIdentityAuthenticator() + .AddIdentityAuthenticator() .AddDefaultIdentityAuthenticators(o => { o.EnableAnonymous = true; @@ -778,35 +813,20 @@ services.AddOpcUa() o.JwksUri = "https://issuer.example/.well-known/jwks.json"; o.Audience = "urn:example:my-server"; }); - -public sealed class MyAuthenticator : IUserTokenAuthenticator -{ - public UserTokenType TokenType => UserTokenType.UserName; - public string? IssuedTokenProfileUri => null; - - public ValueTask AuthenticateAsync( - AuthenticationContext context, CancellationToken ct = default) - { - if (context.TokenHandler is not UserNameIdentityTokenHandler userName) - { - return new ValueTask(AuthenticationResult.NotHandled); - } - - bool ok = userName.UserName == "alice" && - !Utils.Utf8IsNullOrEmpty(userName.DecryptedPassword); - return new ValueTask(ok - ? AuthenticationResult.Accept(new UserIdentity(userName)) - : AuthenticationResult.Reject(new ServiceResult(StatusCodes.BadUserAccessDenied))); - } -} ``` +This uses `StaticUserNameAuthenticator` from the [server-side example](#server-side). +Supply `checkPassword` from your application's credential-verification service before +registering it. It must verify the supplied password against the user's stored credentials; +a recognized username or a nonempty password is not sufficient. Do not log or retain the +decrypted password. + Manual hosts can register against the running server instance. Prefer dependency injection for hosted applications, but this is useful for existing `StandardServer` subclasses: ```csharp -server.CurrentInstance.IdentityRegistry.Register(new MyAuthenticator()); +server.CurrentInstance.IdentityRegistry.Register(new StaticUserNameAuthenticator(checkPassword)); server.CurrentInstance.IdentityRegistry.Register( new JwtAuthenticator(keyResolver, expectedAudience: "urn:example:my-server")); ``` diff --git a/docs/KeyCredentialService.md b/docs/KeyCredentialService.md index af6c5b699b..74947dc9b5 100644 --- a/docs/KeyCredentialService.md +++ b/docs/KeyCredentialService.md @@ -1,19 +1,38 @@ # KeyCredentialService Developer Guide -OPC 10000-12 §8 defines the **KeyCredentialService** — a mechanism for -a GDS to issue and manage credentials (e.g. username/password, API keys, -tokens) on behalf of applications that need to authenticate to -non-OPC UA services such as MQTT brokers or REST APIs. +OPC 10000-12 §8 defines the **KeyCredentialService**. A Global Discovery +Server (GDS) uses it to issue and manage credentials, such as usernames and +passwords, API keys, or tokens, for applications that authenticate to +services outside OPC UA. Examples include Message Queuing Telemetry +Transport (MQTT) brokers and REST APIs. -> **Scope note**: this service is for credentials targeting **non-OPC -> UA** resources (MQTT brokers, REST APIs, etc. — see `ResourceUri` -> + `ProfileUris` on `KeyCredentialServiceType`). The optional bridge +> **Scope note:** This service issues credentials for resources outside +> **OPC UA**, such as MQTT brokers and REST APIs. See `ResourceUri` +> and `ProfileUris` on `KeyCredentialServiceType`. The optional bridge > authenticator described below is **EXPERIMENTAL** under the vendor URI > `urn:opcfoundation:netstandard:profile:authentication:keycredential`; > it is **not** an OPC UA Part 6 §6.5.3 conformance claim. For > standards-conformant OPC UA session authentication, use -> [AuthorizationService](AuthorizationService.md) (JWT) and the -> [Identity Providers](IdentityProviders.md) infrastructure. +> [AuthorizationService](AuthorizationService.md) with JSON Web Tokens +> (JWT) and the [Identity Providers](IdentityProviders.md) infrastructure. + +## Contents + +- [Architecture](#architecture) +- [Client API](#client-api) + - [`KeyCredentialServiceClient`](#keycredentialserviceclient) + - [Discovering the Service NodeId](#discovering-the-service-nodeid) +- [Server-Side: Implementing a Provider](#server-side-implementing-a-provider) + - [`IKeyCredentialRequestStore`](#ikeycredentialrequeststore) + - [Built-in Implementation](#built-in-implementation) + - [Writing a Custom Implementation](#writing-a-custom-implementation) + - [Wiring into the GDS Server](#wiring-into-the-gds-server) +- [Decision matrix: Push vs Pull vs Bridge](#decision-matrix-push-vs-pull-vs-bridge) + - [Hybrid (Push + Pull)](#hybrid-push--pull) +- [Resource Server Push Binding](#resource-server-push-binding) +- [Experimental KeyCredential Issued-Token Bridge](#experimental-keycredential-issued-token-bridge) +- [Audit Events](#audit-events) +- [End-to-End Example](#end-to-end-example) ## Architecture diff --git a/docs/Kubernetes.md b/docs/Kubernetes.md index 5c47c63a75..857f46046e 100644 --- a/docs/Kubernetes.md +++ b/docs/Kubernetes.md @@ -1,18 +1,77 @@ # Kubernetes High Availability Deployment -This guide is the single Kubernetes deployment guide for OPC UA high availability. It covers the base `OPCFoundation.NetStandard.Opc.Ua.Redundancy.Server` features and the opt-in `OPCFoundation.NetStandard.Opc.Ua.Redundancy.Kubernetes` extension: Kubernetes Lease leader election, EndpointSlice peer discovery, and HTTP readiness/liveness driven by OPC UA `ServiceLevel`. +This is the Kubernetes deployment guide for OPC UA high availability. It +covers the base `OPCFoundation.NetStandard.Opc.Ua.Redundancy.Server` +features and the opt-in +`OPCFoundation.NetStandard.Opc.Ua.Redundancy.Kubernetes` extension. The +extension provides Kubernetes Lease leader election, EndpointSlice peer +discovery, and HTTP readiness/liveness driven by OPC UA `ServiceLevel`. See [HighAvailability.md](HighAvailability.md) for the OPC 10000-4 §6.6 redundancy model. The worked server sample is `samples/Redundancy/RedundantServer`. -The API names distinguish standardized OPC UA model wiring from deployment extensions. `AddServerRedundancy(...)`, `AddServerServiceLevel(...)`, and `AddRequestServerStateChange(...)` publish or maintain OPC 10000-4 §6.6 nodes/methods; `UseDistributedAddressSpace(...)`, `UseDistributedSessions(...)`, `UseDistributedSubscriptionMirroring(...)`, `UseKubernetesLeaderElection(...)`, `UseKubernetesPeerDiscovery(...)`, and `UseKubernetesReadiness(...)` register beyond-spec extension services. `AddServerRedundancy(...)` does not drive `Server.ServiceLevel` by itself, so Kubernetes readiness must also register a ServiceLevel provider, commonly with `AddServerServiceLevel(...)` or the leader-aware provider used by the sample. +The API prefixes distinguish standardized OPC UA model wiring from +deployment extensions: + +- `AddServerRedundancy(...)`, `AddServerServiceLevel(...)`, and + `AddRequestServerStateChange(...)` publish or maintain OPC 10000-4 §6.6 + nodes and methods. +- `UseDistributedAddressSpace(...)`, `UseDistributedSessions(...)`, + `UseDistributedSubscriptionMirroring(...)`, + `UseKubernetesLeaderElection(...)`, `UseKubernetesPeerDiscovery(...)`, and + `UseKubernetesReadiness(...)` register beyond-spec extension services. + +`AddServerRedundancy(...)` does not drive `Server.ServiceLevel`. For +Kubernetes readiness, also register a `ServiceLevel` provider, commonly +through `AddServerServiceLevel(...)` or the leader-aware provider used by the +sample. + +## Contents + +- [Redundancy shapes on Kubernetes](#redundancy-shapes-on-kubernetes) +- [Running with Kubernetes API integration](#running-with-kubernetes-api-integration) + - [Application setup with full K8s integration](#application-setup-with-full-k8s-integration) +- [Running without Kubernetes API integration](#running-without-kubernetes-api-integration) + - [Simple setup option](#simple-setup-option) +- [RBAC](#rbac) +- [Services](#services) +- [StatefulSet or Deployment](#statefulset-or-deployment) +- [Readiness and ServiceLevel](#readiness-and-servicelevel) +- [Kubernetes Lease leader election](#kubernetes-lease-leader-election) +- [EndpointSlice peer discovery](#endpointslice-peer-discovery) +- [Secrets, certificates, and trust](#secrets-certificates-and-trust) +- [Time synchronization](#time-synchronization) +- [GDS and NTRS registration](#gds-and-ntrs-registration) +- [Security checklist](#security-checklist) ## Redundancy shapes on Kubernetes -Use **non-transparent redundancy** when each pod has its own OPC UA endpoint and clients use `Server.ServerRedundancy`, `FindServers`, and `ServiceLevel` to fail over. Use **transparent redundancy** when clients connect to one virtual address, typically a `Service` or external load balancer, and the pods share the endpoint URL, application URI, certificate, session state, and subscription state required to hide failover. +Use **non-transparent redundancy** when each pod has its own OPC UA +endpoint. Clients then use `Server.ServerRedundancy`, `FindServers`, and +`ServiceLevel` to fail over. -A multi-pod deployment needs a shared store reachable by all replicas for distributed address-space/session/subscription state. Configure a networked, authenticated, encrypted, capacity-bounded backend — the in-package Raft store, or a CRDT gossip fabric over the pod network: protect the store conduit with authenticated encryption, provision a shared record-protection key through a Kubernetes Secret or CSI-backed secret store, and size/expire session, nonce, and subscription keys so a faulty replica cannot exhaust the backend. +Use **transparent redundancy** when clients connect to one virtual address, +typically a `Service` or external load balancer. To hide failover, the pods +must share the endpoint URL, application URI, certificate, session state, +and subscription state. -A server can run HA on Kubernetes two ways, and you choose one: **with** Kubernetes API integration (Kubernetes-native Lease election, EndpointSlice discovery, and readiness), or **without** it (the in-package Raft/CRDT layers self-manage membership and leadership). The two options are described next. +A multi-pod deployment needs a shared store that all replicas can reach for +distributed address-space, session, and subscription state. Configure a +networked backend with authentication, encryption, and capacity limits, such +as the in-package Raft store or a CRDT gossip fabric over the pod network. +Protect the store connection with authenticated encryption. Provision the +shared record-protection key through a Kubernetes Secret or a secret store +backed by the Container Storage Interface (CSI). Set limits and expiration +for session, nonce, and subscription keys so +a faulty replica cannot exhaust the backend. + +Choose one of two deployment modes: + +- **With Kubernetes API integration:** use Kubernetes-native Lease election, + EndpointSlice discovery, and readiness. +- **Without Kubernetes API integration:** the in-package Raft/CRDT layers + manage membership and leadership. + +The following sections describe both options. ## Running with Kubernetes API integration @@ -64,11 +123,15 @@ services.AddOpcUa() }); ``` -The example above is safe because `UseDistributedAddressSpace(... options.UseLeaderElection = true ...)` -registers a leader-aware `IServiceLevelProvider`. If you compose `UseKubernetesLeaderElection()` and -`UseKubernetesReadiness()` without `UseDistributedAddressSpace()`, register your own -`IServiceLevelProvider` (for example via `AddServerServiceLevel(...)`) before `UseKubernetesReadiness()` -so followers do not stay ready. +The example is safe because +`UseDistributedAddressSpace(... options.UseLeaderElection = true ...)` +registers a leader-aware `IServiceLevelProvider`. + +If you combine `UseKubernetesLeaderElection()` and +`UseKubernetesReadiness()` without `UseDistributedAddressSpace()`, register +your own `IServiceLevelProvider`, for example through +`AddServerServiceLevel(...)`, before `UseKubernetesReadiness()`. This keeps +followers from remaining ready. `UseKubernetesLeaderElection` uses the in-cluster service account token, namespace, and CA mounted at `/var/run/secrets/kubernetes.io/serviceaccount`. Outside Kubernetes it falls back to `SharedStoreLeaseElection` by default (`UseSharedStoreFallback = true`) so local development can still exercise the distributed path. (`UseKubernetes()` registers the shared in-cluster Kubernetes API client that these extensions consume; the leader-election, peer-discovery, and readiness helpers add it automatically, but you can call it directly when adding your own Kubernetes-backed component.) diff --git a/docs/McpServer.md b/docs/McpServer.md index b56a54e8fe..857eaef058 100644 --- a/docs/McpServer.md +++ b/docs/McpServer.md @@ -1,6 +1,41 @@ # OPC UA MCP Server -The OPC UA MCP Server exposes all OPC UA Part 4 service calls as [Model Context Protocol (MCP)](https://modelcontextprotocol.io) tools. This enables AI assistants — Claude, GitHub Copilot, VS Code Copilot, Cursor, and any MCP-compatible client — to connect to OPC UA servers and interact with industrial automation systems through natural language. +The OPC UA MCP Server exposes OPC UA Part 4 service calls as +[Model Context Protocol (MCP)](https://modelcontextprotocol.io) tools. AI +assistants such as Claude, GitHub Copilot, VS Code Copilot, and Cursor can +use these tools to connect to OPC UA servers and interact with industrial +automation systems through natural language. + +## Contents + +- [What It Does](#what-it-does) +- [Tool Profiles](#tool-profiles) + - [Vision-guided Robotics](#vision-guided-robotics) +- [Resources](#resources) + - [Multi-Session Support](#multi-session-support) +- [Installation](#installation) + - [Option 1: Run without installing (.NET 10 SDK)](#option-1-run-without-installing-net-10-sdk) + - [Option 2: Install as a .NET global tool](#option-2-install-as-a-net-global-tool) + - [Option 3: Run from source](#option-3-run-from-source) + - [Option 4: Install from local build](#option-4-install-from-local-build) +- [Configuration](#configuration) + - [Run on demand without a global installation](#run-on-demand-without-a-global-installation) + - [Claude Desktop](#claude-desktop) + - [VS Code / GitHub Copilot](#vs-code--github-copilot) + - [Cursor](#cursor) + - [HTTP Transport (for remote clients)](#http-transport-for-remote-clients) +- [Usage](#usage) + - [Typical Workflow](#typical-workflow) + - [NodeId Formats](#nodeid-formats) + - [Common Well-Known NodeIds](#common-well-known-nodeids) + - [Error Handling](#error-handling) +- [PubSub Tools](#pubsub-tools) +- [Architecture](#architecture) + - [Packages](#packages) + - [Embedding the tools in your own MCP server](#embedding-the-tools-in-your-own-mcp-server) +- [Security Notes](#security-notes) +- [Agent-Usability Quality Gate](#agent-usability-quality-gate) +- [Requirements](#requirements) ## What It Does @@ -463,7 +498,7 @@ the corresponding method NodeId (e.g. `i=14443` for | `pubsub_runtime_read_received` | Read DataSets received by the subscriber | | `pubsub_runtime_status` / `pubsub_runtime_stop` | Status / stop the runtime | -**Discovery** (Part 14 §7.2.4.6 — send a discovery request from the active +**Discovery** (Part 14 §7.2.4.6 — send a discovery request from the active runtime and collect publisher responses): | Tool | Purpose | @@ -472,7 +507,7 @@ runtime and collect publisher responses): | `pubsub_discover_writer_config` | Learn a publisher's WriterGroupId and DataSetWriterIds | | `pubsub_discover_publisher_endpoints` | Learn a publisher's transport endpoint URLs | -**Actions** (Part 14 §7.2.5.6 — request/response over PubSub): +**Actions** (Part 14 §7.2.5.6 — request/response over PubSub): | Tool | Purpose | | --- | --- | @@ -618,7 +653,7 @@ registration extensions. ## Security Notes - The `autoAcceptCerts` parameter is for **testing only**. In production, configure proper certificate trust using the OPC UA certificate stores under `%LocalApplicationData%/OPC Foundation/pki/`. -- The server manages a single OPC UA session at a time. Disconnect before connecting to a different server. +- The server supports simultaneous named OPC UA sessions. Use the intended session identifier for each operation and disconnect that session when finished; connecting to another server does not require closing unrelated sessions. - Application certificates are automatically created on first use and stored in the local certificate store. - Logs are written to `%LocalApplicationData%/OPC Foundation/Logs/McpServer.log.txt`. diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index 0734a80449..181a07841c 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -1,10 +1,43 @@ # Migration Guide -This document is the landing page for migrating your application between -versions of the OPC UA .NET Standard Stack. The detailed per-version -content lives in the [`migrate/`](migrate/) sub-folder; this page is the -index that points you at the right version folder and keeps cross-cutting -migration notes inline. +Use this guide to migrate an application between versions of the OPC UA +.NET Standard Stack. Detailed, version-specific instructions live in the +[`migrate/`](migrate/) subfolder. This page links to those guides and +covers cross-cutting changes. + +## Contents + +- [General principles](#general-principles) +- [Per-version migration index](#per-version-migration-index) +- [Migrating code that used the exposed diagnostics locks](#migrating-code-that-used-the-exposed-diagnostics-locks) + - [Why there is no `[Obsolete]` shim](#why-there-is-no-obsolete-shim) +- [Migrating code that used ILocalNode.DataLock](#migrating-code-that-used-ilocalnodedatalock) +- [Migrating code that used BaseVariableValue.Lock](#migrating-code-that-used-basevariablevaluelock) +- [Migrating code that locked on a NodeState or a NodeBrowser](#migrating-code-that-locked-on-a-nodestate-or-a-nodebrowser) +- [Migrating code that used ApplicationConfiguration.PropertiesLock](#migrating-code-that-used-applicationconfigurationpropertieslock) +- [Migrating node types that override FindChild or CreateChild](#migrating-node-types-that-override-findchild-or-createchild) +- [Adopting replica-consistent NodeIds](#adopting-replica-consistent-nodeids) +- [Removed members on ISession](#removed-members-on-isession) +- [Awaiting custom node-manager cleanup](#awaiting-custom-node-manager-cleanup) +- [Migrating code that called IServerInternal.Set* mutators](#migrating-code-that-called-iserverinternalset-mutators) +- [Migrating IServerStartupTask implementations to IServerContext](#migrating-iserverstartuptask-implementations-to-iservercontext) +- [Removed members on IServerInternal](#removed-members-on-iserverinternal) +- [Migrating servers that relied on unserved history advertisement](#migrating-servers-that-relied-on-unserved-history-advertisement) +- [Migrating custom ISessionManager implementations to ShutdownAsync](#migrating-custom-isessionmanager-implementations-to-shutdownasync) +- [Configuring distributed address-space storage](#configuring-distributed-address-space-storage) +- [Migrating SamplingGroupManager create/modify overrides](#migrating-samplinggroupmanager-createmodify-overrides) +- [Migrating callers of the synchronous MonitoredNode2 notification wrappers](#migrating-callers-of-the-synchronous-monitorednode2-notification-wrappers) +- [Migrating callers of the SecurityPolicies lookup and cryptography statics](#migrating-callers-of-the-securitypolicies-lookup-and-cryptography-statics) +- [Migrating code that drove the server subscription publish pipeline](#migrating-code-that-drove-the-server-subscription-publish-pipeline) +- [Migrating channel subclasses that guarded state with DataLock](#migrating-channel-subclasses-that-guarded-state-with-datalock) +- [Transport resource limits](#transport-resource-limits) +- [Migrating channel subclasses that override HandleIncomingMessage](#migrating-channel-subclasses-that-override-handleincomingmessage) +- [Migrating custom IUserDatabase implementations](#migrating-custom-iuserdatabase-implementations) +- [Migrating from 1.05.377 to 1.05.378](#migrating-from-105377-to-105378) + - [Asynchronous as default](#asynchronous-as-default) + - [Observability](#observability) +- [Migrating from 1.04 to 1.05](#migrating-from-104-to-105) +- [Support](#support) ## General principles @@ -183,63 +216,17 @@ already, so this only affects hand-written derived value classes and callers. ## Migrating code that locked on a NodeState or a NodeBrowser -A `NodeState` guards its own attributes, children, notifiers and references, and -`NodeState.CreateBrowser` guards the browser it builds. Nothing in the stack -takes a lock on a node instance any more, so neither should a caller: +Remove external `lock (node)` statements: `NodeState` synchronizes its own +attributes and collections. Replace reference check-then-add pairs with +`AddReferenceIfMissing`. Browsers are single-consumer, and `NodeBrowser.DataLock` +is removed (analyzer `UA0027`). -```csharp -// was — the node manager reached for the node's monitor from outside the node -lock (source) -{ - browser = source.CreateBrowser(context, view, referenceType, includeSubtypes, - browseDirection, default, null, false); -} - -// now -INodeBrowser browser = source.CreateBrowser(context, view, referenceType, - includeSubtypes, browseDirection, default, null, false); -``` - -`lock (node)` was also the only way to make a check-then-act pair atomic. Use -`NodeState.AddReferenceIfMissing` instead, which does the check and the insert -under the node's own lock: - -```csharp -// was -lock (node) -{ - if (!node.ReferenceExists(ReferenceTypeIds.HasNotifier, true, ObjectIds.Server)) - { - node.AddReference(ReferenceTypeIds.HasNotifier, true, ObjectIds.Server); - } -} - -// now -node.AddReferenceIfMissing(ReferenceTypeIds.HasNotifier, true, ObjectIds.Server); -``` - -`NodeBrowser.DataLock` is gone with it. A browser is **single-consumer**: it -belongs to whoever created it and performs no synchronization of its own. A -derived browser that took `DataLock` inside its `Next()` override drops the -`lock` statement and keeps the body. Where a browser genuinely outlives one -service call — the instance parked in a continuation point for `BrowseNext` — -its owner serializes it, as the stack does for its own continuation points. - -A node type that overrides `CreateBrowser` and builds its own browser instead of -delegating to the base implementation must fill it through -`PopulateBrowserSynchronized` rather than calling `PopulateBrowser` directly, -otherwise its browser is assembled from separately locked reads and can observe -a node halfway through a change. - -Analyzer `UA0027` flags `NodeBrowser.DataLock`. See -[migrate/2.0.x/node-states.md](migrate/2.0.x/node-states.md). - -`INodeBrowser` also gains `NextAsync(CancellationToken)`, which the async server -browse and translate-path loops use to iterate a browser. The default completes -synchronously with `Next()`, so existing browsers are unaffected; a browser whose -references come from I/O overrides `NextAsync` and awaits there instead of -blocking inside `Next()`. Details in -[migrate/2.0.x/node-states.md](migrate/2.0.x/node-states.md#nodebrowser-gains-an-async-iteration-seam). +The [node-state migration guide](migrate/2.0.x/node-states.md#nodestate-guards-itself-nodebrowser-is-single-consumer-ua0027) +contains the before/after examples and the `PopulateBrowserSynchronized` +requirement for custom browsers. For I/O-backed iteration, see +[the async iteration seam](migrate/2.0.x/node-states.md#nodebrowser-gains-an-async-iteration-seam). +The current [threading contract](NodeManagers.md#threading-contract-for-nodes-and-browsers) +defines snapshot and synchronization boundaries. ## Migrating code that used ApplicationConfiguration.PropertiesLock @@ -704,68 +691,18 @@ a warning rather than an error. The `ILogger` argument on the `Encrypt` and ## Migrating code that drove the server subscription publish pipeline -Twelve members left the server `Opc.Ua.Server.ISubscription`, and the -`SessionPublishQueue` class became internal. The publish pipeline — timer -expiry, message acknowledgement, notification consumption, session release — -is now driven exclusively by `SubscriptionManager` and the publish queue -through an internal contract that only `Subscription` implements. Any holder -of an `ISubscription` (for example via -`IServerInternal.SubscriptionManager.GetSubscriptions()`) could previously -call these members and corrupt the publishing state machine: consume -notifications a client never saw, advance sequence numbers, or release a -subscription its session still owned. +The server publish pipeline is internal. Analyzer `UA0030` identifies removed +`Opc.Ua.Server.ISubscription` members: remove no-op calls and use the service +operations for publishing, acknowledgements, transfers, and session teardown. +Custom server subscriptions must derive from `Subscription`. -**Deleted outright — remove the call.** `ItemReadyToPublish` and -`ItemNotificationsAvailable` had commented-out bodies since 1.5.x, so every -call was already a no-op. The obsolete parameterless `SessionClosed()` is -gone with them, and so is `TransferSessionAsync`: the server transfers -subscriptions through its internal claim/prepare/commit protocol, reached -via the `TransferSubscriptions` service -(`ISubscriptionManager.TransferSubscriptionsAsync`) — the one-shot direct -transfer bypassed the reservation that protects a transfer against a -concurrently closing source session. +See the [server subscription migration](migrate/2.0.x/sessions-subscriptions.md#opcuaserverisubscription-the-publish-pipeline-is-server-internal) +for the complete removed-member list, replacement service paths, and custom +implementation requirements. `SessionPublishQueue` is internal too; applications +must not drive it directly. -**Internalized — use the service operations.** `PublishTimerExpired`, -`Acknowledge`, `PublishTimeout`, `SubscriptionTransferred`, -`AvailableSequenceNumbersForRetransmission`, `QueueOverflowHandler`, -`SessionClosed(ISession)` and `Publish` are no longer on the interface. -Code that called them was reimplementing a slice of the server; the -supported path is the service set (`Publish`, `Republish`, -`TransferSubscriptions`) and the `ISubscriptionManager` surface -(`SessionClosingAsync` for session teardown). `ResendData`, -`GetMonitoredItems` and the monitored-item service operations remain on -`ISubscription` — resolve the subscription with -`ISubscriptionManager.TryGetSubscription` first, the way the -`Server_ResendData` method handler does. - -```csharp -// was: drive the pipeline directly -if (server.SubscriptionManager.TryGetSubscription(subscriptionId, out ISubscription? subscription)) -{ - subscription.PublishTimerExpired(); - subscription.Acknowledge(context, sequenceNumber); -} - -// now: the pipeline is server-internal; acknowledgements travel with the -// Publish service request, and the publish timer belongs to the server -Task response = server.SubscriptionManager.PublishAsync( - context, subscriptionAcknowledgements, parkSink, cancellationToken); -``` - -**Custom subscription implementations must derive from `Subscription`.** -`SubscriptionManager.CreateSubscription` and `RestoreSubscriptionAsync` -still return `ISubscription`, but an override returning a type that does not -derive from `Subscription` now fails at creation with `Bad_InternalError` -instead of publishing partially (transfer already required the concrete -type). Derive from `Subscription` and override the behaviour you need; the -pipeline members are explicit implementations of the internal contract and -are not virtual. - -The no-`[Obsolete]`-shim rule [above](#why-there-is-no-obsolete-shim) -applies here for the same reason: `ISubscription` is implemented by -downstream code, and re-adding an interface member later would break every -implementer. Analyzer `UA0030` flags each removed member on the migration -path and names the replacement. +The [no-obsolete-shim rule](#why-there-is-no-obsolete-shim) applies because +restoring members to a public interface would break downstream implementers. ## Migrating channel subclasses that guarded state with DataLock diff --git a/docs/ModelChangeTracking.md b/docs/ModelChangeTracking.md index ea03a03b94..53efb407a6 100644 --- a/docs/ModelChangeTracking.md +++ b/docs/ModelChangeTracking.md @@ -1,19 +1,19 @@ # Model Change Tracking -OPC UA Part 5 §6.4.32 defines `GeneralModelChangeEventType` so a -server can notify clients that the address space has changed — -nodes added or deleted, references added or deleted, datatypes -changed. The `Opc.Ua.Client.ModelChange` namespace ships an -opt-in client-side tracker that consumes those events, drops the -affected entries from the client's `INodeCache`, and surfaces the -changes through a strongly-typed event so application code can -re-browse the impacted subtrees. +OPC UA Part 5 §6.4.32 defines `GeneralModelChangeEventType`. Servers use it +to notify clients about address-space changes, including added or deleted +nodes and references, and changed data types. -The server-side counterpart lives in `Opc.Ua.Server.Alarms`: -`ModelChangeAggregator` batches per-call changes and the -`CustomNodeManager` (and `AsyncCustomNodeManager`) emit -`GeneralModelChangeEvent` automatically from `CreateNode` / -`DeleteNode`. Opt out via a flag if you need manual control. +The `Opc.Ua.Client.ModelChange` namespace provides an opt-in client-side +tracker. It consumes these events, removes affected entries from the +client's `INodeCache`, and surfaces the changes through a strongly typed +event. Application code can then browse the affected subtrees again. + +The server-side counterpart lives in `Opc.Ua.Server.Alarms`. +`ModelChangeAggregator` batches per-call changes. `CustomNodeManager` and +`AsyncCustomNodeManager` automatically emit `GeneralModelChangeEvent` from +`CreateNode` and `DeleteNode`. Set a flag to opt out when you need manual +control. - [Quick reference](#quick-reference) - [Server side: emitting model changes](#server-side-emitting-model-changes) @@ -70,8 +70,11 @@ public class MyNodeManager : AsyncCustomNodeManager ``` > **Browse consistency.** `CreateNodeAsync`, `AddNodeAsync` and `DeleteNodeAsync` also keep the node manager's internal component cache in sync with the change. -> A deleted node is evicted from the cache, and the parent's cached view is refreshed after a runtime add or remove, so a Browse, Read or Call issued afterwards reflects the committed child set instead of a stale, cached view. -> Re-registering or replacing a node id (for example swapping a passive child for a typed proxy) refreshes the cached instance only when that node is already cached. +> A deleted node is evicted from the cache. After a runtime add or remove, the +manager refreshes the parent's cached view. A subsequent Browse, Read, or +Call therefore sees the committed child set instead of stale data. +> When the node is already cached, re-registering it refreshes the cached +instance. Replacing a cached node with a typed proxy also refreshes it. If your node manager mutates the address space without going through `CreateNodeAsync` / `DeleteNodeAsync` (for example by editing an @@ -264,10 +267,10 @@ reports the address-space change. Without a refresh, a Client resolves NodeIds from the new namespace against a stale `NamespaceUris` table and silently gets the wrong index. -The tracker closes that gap. Given an `INamespaceTableRefresher` it -re-reads the namespace table before it invalidates the cache and before -it raises `ModelChanged`, so subscribers that re-browse from the handler -already see the new uris: +The tracker closes that gap when given an `INamespaceTableRefresher`. It +refreshes the namespace table before invalidating the cache and raising +`ModelChanged`. Subscribers can then re-browse from the event handler using +the new URIs: ```csharp public interface INamespaceTableRefresher @@ -351,7 +354,6 @@ OPC UA Part 5 §9.32.2 ties `ModelChangeEvent` emission to the > changed, a `ModelChangeEvent` shall be generated. A Server shall > support both the `ModelChangeEvent` and the `NodeVersion` Property > or neither, but never only one of the two mechanisms." -> > "Only those Nodes of the AddressSpace having a `NodeVersion` shall > trigger a `ModelChangeEvent`. Other Nodes shall not trigger a > `ModelChangeEvent`." diff --git a/docs/ModelDependencies.md b/docs/ModelDependencies.md index caa17b40ec..6d32012fa0 100644 --- a/docs/ModelDependencies.md +++ b/docs/ModelDependencies.md @@ -1,5 +1,12 @@ # Cross-Assembly Model Dependencies +## Contents + +- [Attribute shape](#attribute-shape) +- [Payload wire format (`ModelDependencyV1`)](#payload-wire-format-modeldependencyv1) +- [Diagnostics](#diagnostics) +- [Implementation](#implementation) + The OPC UA source generator emits `[assembly: Opc.Ua.ModelDependencyAttribute(...)]` metadata on every assembly that has nodesets or design files in ``. A single attribute carries both the lightweight dependency-closure information diff --git a/docs/NativeAoT.md b/docs/NativeAoT.md index 634f8526a6..ac26a73daf 100644 --- a/docs/NativeAoT.md +++ b/docs/NativeAoT.md @@ -1,20 +1,121 @@ -# Native AOT Testing +# Native AOT -## Overview +[Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) +(ahead-of-time) compilation publishes an application and the parts of the SDK +that it uses as a single native executable. The executable starts quickly and +runs without an installed .NET runtime, but it supports only code that the +compiler can analyze at publish time. This guide first shows how to publish an +application, then describes the test harness that verifies the stack under +Native AOT. -The OPC UA .NET Standard stack supports -[Native AOT (Ahead-of-Time) compilation](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8plus), -which produces a self-contained executable that is compiled to native code at -publish time rather than at run time. This eliminates the need for a JIT -compiler and the .NET runtime on the target machine, resulting in faster startup -and a smaller deployment footprint. +## Contents + +- [Publish an application](#publish-an-application) + - [Enable Native AOT](#enable-native-aot) + - [Publish and verify the executable](#publish-and-verify-the-executable) + - [Use AOT-compatible features](#use-aot-compatible-features) +- [Test harness overview](#test-harness-overview) +- [Prerequisites](#prerequisites) + - [.NET SDK](#net-sdk) + - [Platform-Specific Native Toolchain](#platform-specific-native-toolchain) +- [Project Structure](#project-structure) + - [Why TUnit Instead of NUnit?](#why-tunit-instead-of-nunit) + - [Test Fixture Pattern](#test-fixture-pattern) +- [How to Build and Run](#how-to-build-and-run) + - [Publish the Native AOT Binary](#1-publish-the-native-aot-binary) + - [Run the Tests](#2-run-the-tests) + - [Build + Run in a Single Step (Development)](#build--run-in-a-single-step-development) +- [CI Integration](#ci-integration) +- [Writing New AOT Tests](#writing-new-aot-tests) + - [Choose or Create a Test Class](#1-choose-or-create-a-test-class) + - [Use TUnit Attributes and Assertions](#2-use-tunit-attributes-and-assertions) + - [Keep Code AOT-Compatible](#3-keep-code-aot-compatible) + - [Handle Trimming Warnings](#4-handle-trimming-warnings) +- [Troubleshooting](#troubleshooting) + - [`'vswhere.exe' is not recognized` on Windows](#vswhereexe-is-not-recognized-on-windows) + - [Publish Fails with Linker Errors](#publish-fails-with-linker-errors) + - [`TypeInitializationException` or `MissingMetadataException` at Runtime](#typeinitializationexception-or-missingmetadataexception-at-runtime) + - [Tests Pass Under `dotnet test` but Fail Under AOT](#tests-pass-under-dotnet-test-but-fail-under-aot) + - [Slow Publish Times](#slow-publish-times) + - [`IL2104` or Other Trimming Warnings](#il2104-or-other-trimming-warnings) + +## Publish an application + +### Enable Native AOT + +Install the [native toolchain](#platform-specific-native-toolchain) for your +platform. Then set `PublishAot` in the application's project file: + +```xml + + Exe + net10.0 + true + +``` + +The stack's Native AOT tests run on `net10.0`. `dotnet build` and `dotnet run` +still use the JIT compiler; only `dotnet publish` compiles the native +executable. + +### Publish and verify the executable + +Publish for the runtime identifier of the target platform, such as `win-x64`, +`linux-x64`, `linux-arm64`, or `osx-arm64`: + +```bash +dotnet publish -c Release -r win-x64 +``` + +The `bin/Release/net10.0//publish` folder then contains one +native executable and its symbol file. If the folder instead contains the +application's `.dll` and `coreclr.dll`, the publish did not use Native AOT. +Check that `PublishAot` applies to the target framework you publish, delete the +`bin` and `obj` folders, and publish again. + +The client and server from +[Getting started](GettingStarted.md#route-b-build-applications-from-nuget-packages) +publish this way without trim or AOT warnings. A published application is also +a new instance for its certificate stores when `PkiRoot` depends on the working +directory, as in Getting started. Run it from the folder that contains its +`pki` folder, or trust its new certificate again. + +### Use AOT-compatible features + +Fix every trim and AOT warning (`IL2026`, `IL3050`, and related codes) that the +publish reports for your code. The following features are AOT-compatible: + +- Source-generated models and node managers; see + [Node managers: NativeAOT publishing](NodeManagers.md#nativeaot-publishing). +- Source-generated data types; see + [Source-generated data types](SourceGeneratedDataTypes.md). +- The default complex-type builder. The optional Reflection.Emit builder + requires the JIT compiler; see [Complex types: type builders](ComplexTypes.md#type-builders). +- Hosting with dependency injection; see + [Dependency injection: Native AOT](DependencyInjection.md#native-aot). +- PubSub; see [PubSub: Native AOT](PubSub.md#native-aot). +- The crypto provider model; see [Crypto providers](CryptoProvider.md). + +In your own code, avoid unbounded reflection and runtime code generation; see +[Keep code AOT-compatible](#3-keep-code-aot-compatible). + +Do not enable globalization-invariant mode for a server, a common size +optimization for native executables. With the `InvariantGlobalization` property +or the `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT` environment variable set, the +server does not start: it reports a `CultureNotFoundException` for the `en-US` +culture. Clients run in this mode. + +## Test harness overview The **Opc.Ua.Aot.Tests** project verifies that the core OPC UA libraries work -correctly when published as a Native AOT binary. The tests exercise encoding, -sessions, subscriptions, monitored items, discovery, security, events, history, -diagnostics, batch operations, node cache, complex types, GDS client operations, -and client sample patterns — all running inside a single ahead-of-time compiled -executable. +correctly when published as a Native AOT binary. The tests cover: + +- Encoding, sessions, subscriptions, and monitored items. +- Discovery, security, events, and history. +- Diagnostics, batch operations, and the node cache. +- Complex types, GDS client operations, and client sample patterns. + +All tests run inside a single ahead-of-time compiled executable. The crypto provider model is AOT-compatible and covered by `CryptoProviderAotTests`; see [CryptoProvider](CryptoProvider.md). The optional @@ -216,6 +317,17 @@ code, fix the root cause rather than suppressing. ## Troubleshooting +### `'vswhere.exe' is not recognized` on Windows + +The native link step locates the Visual C++ tools with `vswhere.exe`. If the +publish fails with `'vswhere.exe' is not recognized as an internal or external +command`, add the Visual Studio Installer folder to `PATH` and publish again: + +```powershell +$env:PATH = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer;$env:PATH" +dotnet publish -c Release -r win-x64 +``` + ### Publish Fails with Linker Errors Ensure the platform-specific C/C++ toolchain is installed (see diff --git a/docs/NodeIdAssignment.md b/docs/NodeIdAssignment.md index e6ffc335f4..0e3fa0f459 100644 --- a/docs/NodeIdAssignment.md +++ b/docs/NodeIdAssignment.md @@ -47,19 +47,20 @@ NodeId NextCounterNodeId(); NodeId CreateChildNodeId(NodeId parentNodeId, QualifiedName browseName, ushort namespaceIndex, NamespaceTable namespaceUris); ``` -The namespace is why this is a contract rather than constructor -configuration: a namespace belongs to the NodeManager, not to the node, and -a manager learns its own namespace index only after the server's namespace -table has been extended — which is later than a factory registered in -dependency injection was built. Implementations are therefore immutable and -the `With…` methods return a view, so one registered instance serves -managers that own different namespaces. +The factory needs the NodeManager's namespace index, but the server adds that +namespace only after constructing the manager. A factory registered through +dependency injection is therefore created too early to receive the final +index as constructor configuration. -`AsyncCustomNodeManager.NodeIdFactory` is typed as this interface, so a -caller can put its own rule in front of `DefaultNodeIdFactory` by -decorating it — claim one subtree, delegate the rest — instead of -overriding `New` on the NodeManager and scattering the identifier rule -across it. +For that reason, `IRebasableNodeIdFactory` is a contract rather than +constructor configuration. Implementations are immutable, and the `With…` +methods return views with different namespaces. One registered factory can +serve managers that own different namespaces. + +`AsyncCustomNodeManager.NodeIdFactory` uses this interface. A caller can +decorate `DefaultNodeIdFactory` with a custom rule—for example, claim one +subtree and delegate other nodes—instead of overriding `New` on the +NodeManager and scattering the rule across it. The property is nullable, and null means *do not assign*. A node copy deliberately hides the factory from the children it materialises, so code @@ -107,11 +108,11 @@ ordering. The index form is a fallback for an unregistered namespace; it is deliberately distinct so identifiers minted before the namespace was registered cannot collide with the ones minted after. -The path is scratch. In every mode but `String` it is hashed and thrown -away, so it is built into a stack buffer — or, when it does not fit, one -rented from `ArrayPool` — and never becomes a string at all. The -length is measured and written by one routine, so the length reserved for -a segment cannot drift from the length written into it. +The path is temporary. Except in `String` mode, the factory hashes it and +discards it without creating a string. It builds the path in a stack +buffer, or rents a buffer from `ArrayPool` when needed. One routine +measures and writes the path, so the reserved segment length always +matches the written length. ### Modes @@ -157,24 +158,23 @@ could pass as a re-mint. Three consequences worth knowing: -- The record is kept per namespace, because identifiers in different - namespaces cannot collide, and is shared by every view derived from one - factory — so two NodeManagers that rebase the registered factory onto - the same namespace are checked against each other, and draw sequential - identifiers from one counter rather than from two seeded moments apart. - Two NodeManagers that each construct a factory of their own instead of - taking the registered one share nothing, and have no way to. -- It is never pruned. An identifier handed to a client stays spoken for - even after the node goes away, so re-minting it for a different path is - exactly the collision this catches. It costs roughly 50 bytes per - distinct path minted. -- It covers what the factory mints, not identifiers a caller assigned - itself. `String` cannot collide and keeps no record. `Counter` cannot - collide with itself either, but it mints into the same numeric space a - hash does, so when checking is asked for it reserves what it hands out — - otherwise a namespace minting in both modes could issue one identifier - twice. It steps over a taken value rather than reporting it, because the - counter is the side free to move. +- The factory keeps a record per namespace because identifiers in + different namespaces cannot collide. Views derived from one factory + share that record. Therefore, two NodeManagers that rebase the registered + factory onto the same namespace are checked against each other and draw + sequential identifiers from one counter. NodeManagers that construct + separate factories instead of using the registered factory do not share + records. +- The factory never prunes the record. An identifier handed to a client + remains reserved after its node is removed, so re-minting it for another + path is a collision. The record costs roughly 50 bytes per distinct path. +- It covers identifiers the factory mints, not identifiers a caller + assigns. `String` identifiers cannot collide, so the factory keeps no + record for them. `Counter` identifiers cannot collide with each other, + but they use the same numeric namespace as hashes. When checking is + enabled, the factory reserves each counter value to prevent another + mode from minting it. If a counter value is already taken, the factory + advances to the next value because the counter can move. `Counter` is for nodes whose browse paths repeat over time — per-session diagnostics objects, inference jobs, rediscovered assets. Its counter diff --git a/docs/NodeManagement.md b/docs/NodeManagement.md index 866cbf6fef..75dd2e5777 100644 --- a/docs/NodeManagement.md +++ b/docs/NodeManagement.md @@ -14,6 +14,18 @@ The SDK ships full request validation, per-item dispatch, audit-event emission, and per-NodeManager opt-in. NodeManagers do not need to override the four `StandardServer` service methods themselves. +## Contents + +- [Architecture](#architecture) +- [Opting in: `INodeManagementAsyncNodeManager`](#opting-in-inodemanagementasyncnodemanager) +- [Dispatch and routing rules](#dispatch-and-routing-rules) +- [Request handling on `AsyncCustomNodeManager`](#request-handling-on-asynccustomnodemanager) +- [Error codes returned](#error-codes-returned) +- [Audit events](#audit-events) +- [Client-side example](#client-side-example) +- [Custom behavior](#custom-behavior) +- [See also](#see-also) + ## Architecture 1. The client request lands on `StandardServer.AddNodesAsync` / @@ -21,16 +33,17 @@ override the four `StandardServer` service methods themselves. 2. Each service override validates the request and forwards the per-item collection to the matching `IMasterNodeManager.AddNodesAsync` etc. dispatcher. -3. `MasterNodeManager` looks up the owning NodeManager for every item - (the requested NodeId namespace, or the BrowseName namespace when the - Server assigns the NodeId, for `AddNodes`; node for `DeleteNodes`; and - source for reference changes) - and validates the matching `PermissionType` before asking it to - perform the work via the optional `INodeManagementAsyncNodeManager` - interface. `AddNodes` requires `AddNode` in the target namespace's - default policy and `AddReference` on the parent. Every Node permission - is evaluated together with the Node's applicable - `AccessRestrictions`. +3. For each item, `MasterNodeManager` selects the owning NodeManager: + - `AddNodes`: the requested NodeId namespace, or the BrowseName + namespace if the server assigns the NodeId. + - `DeleteNodes`: the node's owning NodeManager. + - Reference changes: the source node's owning NodeManager. + + It validates the matching `PermissionType`, then asks the optional + `INodeManagementAsyncNodeManager` interface to perform the operation. + `AddNodes` requires `AddNode` in the target namespace's default policy + and `AddReference` on the parent. The server evaluates each Node + permission together with the Node's applicable `AccessRestrictions`. 4. The aggregated per-item results are wrapped in the matching response envelope and the audit event (`AuditAddNodesEvent`, `AuditDeleteNodesEvent`, `AuditAddReferencesEvent`, @@ -123,7 +136,7 @@ The default implementation honors the following request fields: | `BrowseName` | Must be non-null. `BadBrowseNameDuplicated` when a sibling under the same parent already uses the same browse name — both local children and previously-added cross-NodeManager children attached to the same parent by this NodeManager are checked. | | `ParentNodeId` | Used to attach the new child to the parent. Cross-NodeManager parents are supported — the forward `parent → child` edge is added via the master so the parent's NodeManager records it, and the inverse `child → parent` edge is added on the new node. | | `ReferenceTypeId` | Must be a `HierarchicalReferences` subtype. | -| `NodeAttributes` | Optional `VariableAttributes` / `ObjectAttributes` are applied to the new node (`DisplayName`, `Description`, `DataType`, `ValueRank`, `AccessLevel`, `UserAccessLevel`, `Historizing`, `MinimumSamplingInterval`, `Value`, `EventNotifier`). | +| `NodeAttributes` | Optional `VariableAttributes` or `ObjectAttributes` are applied to the new node. Supported fields include `DisplayName`, `Description`, `DataType`, `ValueRank`, `AccessLevel`, `UserAccessLevel`, `Historizing`, `MinimumSamplingInterval`, `Value`, and `EventNotifier`. | | `DeleteTargetReferences` | When true, `DeleteNodes` also removes references on other NodeManagers that target the deleted node. | | `DeleteBidirectional` | When true on `DeleteReferences`, the inverse edge is also deleted only for an explicitly local target. Remote targets remain source-side operations. | @@ -176,11 +189,11 @@ service contract. ## Custom behavior -To customize behavior for a particular NodeManager (e.g. enforce a -custom NodeId scheme, persist the new node, or veto specific node -classes), override the matching method on -`INodeManagementAsyncNodeManager` after opting in. For example, to -restrict additions to `BaseObjectState`: +After opting in, override the matching +`INodeManagementAsyncNodeManager` method to customize a NodeManager. For +example, you can enforce a custom NodeId scheme, persist a new node, or veto +specific node classes. The following example restricts additions to +Object nodes: ```csharp public override async ValueTask<(ServiceResult result, NodeId addedNodeId)> AddNodeAsync( diff --git a/docs/NodeManagers.md b/docs/NodeManagers.md index 0a9f71ca5c..552e83aba6 100644 --- a/docs/NodeManagers.md +++ b/docs/NodeManagers.md @@ -1,35 +1,16 @@ # Node Managers -Event delivery for a node follows enqueue order. The Server node and -nodes opting into parallel event consumers deliver each event concurrently -to independent monitored items, then complete that event before advancing -the shared queue. A slow permission check cannot let a later event overtake -an earlier one; the bounded queue still applies backpressure. - -Monitored-item deletion and mode changes isolate failures by owner. -Already completed results are retained, remaining items for a failed -owner receive an explicit error, and other owners still run. Event -unsubscribe failures likewise do not skip later owners or monitored items. -Request cancellation continues to stop the operation rather than being -converted into an ordinary per-item failure. - -Fluent node builders reject new handler registration once graph authoring -is sealed, including callbacks wired directly to node slots. History, -removal, condition-refresh, and monitored-item callbacks on ad-hoc child -and instance builders use the same dispatcher as regular node builders. -A manager without that dispatcher rejects those registrations explicitly. - -Monitored-source lifecycle ownership is rechecked after a worker stops. -A superseded deactivate/reactivate cycle keeps its live source rather than -reacquiring it and releasing the replacement. Callback invocation is -coordinated without holding a gate across its asynchronous completion, so -first-subscriber callbacks can still reenter monitoring operations. -Virtual monitored sources register the same shutdown behavior as concrete -sources; every live materialized instance is released at teardown. +A node manager exposes part of a server's address space: variables, methods, +events, and their behavior. Start with the [overview](#overview) and +[first model](#start-with-a-model), then use the architecture and lifecycle +sections when you need custom or dynamically loaded models. If you have not run +a server yet, start with [Getting started](GettingStarted.md). ## Table of contents - [Overview](#overview) +- [Start with a model](#start-with-a-model) + - [Single-file `Program.cs` — what it looks like](#single-file-programcs--what-it-looks-like) - [Built-in node managers](#built-in-node-managers) - [Master node manager](#master-node-manager) - [Core node manager](#core-node-manager) @@ -42,16 +23,20 @@ sources; every live materialized instance is released at teardown. - [Operational behaviour](#operational-behaviour) - [Reading & Writing](#reading--writing) - [Method Calls](#method-calls) - - [Runtime subtype replacement (IPredefinedNodeSubtypeReplacer)](#runtime-subtype-replacement-ipredefinednodesubtypereplacer) + - [Browsing and path translation](#browsing-and-path-translation) + - [Runtime subtype replacement (`IPredefinedNodeSubtypeReplacer`)](#runtime-subtype-replacement-ipredefinednodesubtypereplacer) - [Monitoring and subscriptions](#monitoring-and-subscriptions) - [Sampling interval revision](#sampling-interval-revision) - [History](#history) - [Security](#security) - [Threading contract for nodes and browsers](#threading-contract-for-nodes-and-browsers) + - [Delivery and callback lifecycle](#delivery-and-callback-lifecycle) - [Registering node managers](#registering-node-managers) - [Startup registration](#startup-registration) + - [Registration generations](#registration-generations) - [Runtime registration](#runtime-registration) - [Node manager lifecycle impact on clients](#node-manager-lifecycle-impact-on-clients) + - [Reload modes](#reload-modes) - [MonitoredItems](#monitoreditems) - [Continuation points](#continuation-points) - [Namespaces](#namespaces) @@ -66,27 +51,26 @@ sources; every live materialized instance is released at teardown. - [See also](#see-also) - [Source-generated node managers](#source-generated-node-managers) - [What the generator produces](#what-the-generator-produces) + - [The generated activation pipeline](#the-generated-activation-pipeline) - [Opting in](#opting-in) - - [Per-class opt-in via [NodeManager] (recommended)](#per-class-opt-in-via-nodemanager-recommended) + - [Per-class opt-in via `[NodeManager]` (recommended)](#per-class-opt-in-via-nodemanager-recommended) - [Taking over the constructor and the namespace order](#taking-over-the-constructor-and-the-namespace-order) - - [Asynchronous startup before Configure](#asynchronous-startup-before-configure) - [Binding to a model a referenced assembly supplies](#binding-to-a-model-a-referenced-assembly-supplies) - [Project-wide opt-in via MSBuild property (legacy)](#project-wide-opt-in-via-msbuild-property-legacy) - - [Wiring callbacks: the Configure partial](#wiring-callbacks-the-configure-partial) + - [Wiring callbacks: the `Configure` partial](#wiring-callbacks-the-configure-partial) - [Addressing modes](#addressing-modes) - [On-demand virtual node families](#on-demand-virtual-node-families) - [Monitored-item creation and lifecycle](#monitored-item-creation-and-lifecycle) - [Creating nodes under other managers' nodes (Objects folder)](#creating-nodes-under-other-managers-nodes-objects-folder) - - [Creating nodes from scratch — the Add* surface](#creating-nodes-from-scratch--the-add-surface) - - [Typed model-traversal — the Configure(I{Manager}NodeManagerBuilder) partial](#typed-model-traversal--the-configureimanagernodemanagerbuilder-partial) + - [Creating nodes from scratch — the `Add*` surface](#creating-nodes-from-scratch--the-add-surface) + - [Typed model-traversal — the `Configure(I{Manager}NodeManagerBuilder)` partial](#typed-model-traversal--the-configureimanagernodemanagerbuilder-partial) - [What the generator emits per model](#what-the-generator-emits-per-model) - - [Methods with arguments — typed OnCall overloads](#methods-with-arguments--typed-oncall-overloads) - - [Event sources — typed Publish<TEvent> on notifier wrappers](#event-sources--typed-publishtevent-on-notifier-wrappers) + - [Methods with arguments — typed `OnCall` overloads](#methods-with-arguments--typed-oncall-overloads) + - [Event sources — typed `Publish` on notifier wrappers](#event-sources--typed-publishtevent-on-notifier-wrappers) - [Where the typed overload appears](#where-the-typed-overload-appears) - [Two registration shapes](#two-registration-shapes) - - [Tuning lifecycle with EventPublishOptions](#tuning-lifecycle-with-eventpublishoptions) + - [Tuning lifecycle with `EventPublishOptions`](#tuning-lifecycle-with-eventpublishoptions) - [Hand-written node managers](#hand-written-node-managers) - - [Single-file Program.cs — what it looks like](#single-file-programcs--what-it-looks-like) - [Multi-namespace and manager-swap subclassing](#multi-namespace-and-manager-swap-subclassing) - [NativeAOT publishing](#nativeaot-publishing) - [Runtime NodeSet alternative](#runtime-nodeset-alternative) @@ -105,6 +89,7 @@ sources; every live materialized instance is released at teardown. - [Importing a NodeSet2 overlay at runtime — `builder.Import`](#importing-a-nodeset2-overlay-at-runtime--builderimport) - [NodeSet2 access-level bitmasks](#nodeset2-access-level-bitmasks) - [Materialising instances at runtime — NodeId assignment](#materialising-instances-at-runtime--nodeid-assignment) + - [Custom node types and assignment control](#custom-node-types-and-assignment-control) - [Current limitations](#current-limitations) - [Sample](#sample) @@ -118,6 +103,71 @@ Developers need to care about node managers when they expose application data, m The server builds the initial set of node managers before accepting connections. Additional managers can be registered by hosting extensions such as `AddNodeManager` and `AddRuntimeNodeSet`, and the lifecycle API can add, reload, or remove lifecycle-managed managers while the server is running. Regardless of how a manager is supplied, it must cooperate with the master node manager's routing and reference-merging rules so clients can browse, monitor, and call nodes consistently across namespace and manager boundaries. +## Start with a model + +For a first server, use the +[MinimalBoilerServer sample](../samples/MinimalApi/MinimalBoilerServer). +It supplies a model and a generated node-manager factory, so you can start +with variables and callbacks rather than implementing service dispatch. +To write a model of your own and use it from a client, follow the +[Your first information model](FirstModel.md) tutorial. + +1. Follow [source-generated node managers](#source-generated-node-managers) + to include your model and generate its factory. +2. Register the factory with `AddOpcUa().AddServer(...).AddNodeManager()`; + the [single-file host example](#single-file-programcs--what-it-looks-like) + shows the complete hosting shape. +3. Attach value and method handlers in the generated `Configure` partial, then + connect a client and browse the model. + +If models change independently of the binary, start with +[runtime NodeSets](RuntimeNodeSets.md#quick-start-examples) instead. +Both paths use the same routing, permissions, and lifecycle contracts below. + +### Single-file `Program.cs` — what it looks like + +The shipping `services.AddOpcUa().AddServer(...)` extension wires the +server into the .NET Generic Host: configuration, certificate check, +`ApplicationInstance` lifetime and Ctrl+C/SIGTERM handling are all owned +by the host. User code stays at ~12 lines. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); +builder.Logging.AddConsole(); + +builder.Services + .AddOpcUa() + .AddServer(o => + { + o.ApplicationName = "MyServer"; + o.ApplicationUri = "urn:localhost:MyServer"; + o.ProductUri = "uri:opcfoundation.org:MyServer"; + o.AutoAcceptUntrustedCertificates = true; + o.EndpointUrls.Add("opc.tcp://localhost:51210/MyServer"); + }) + .AddNodeManager(); + +await builder.Build().RunAsync(); +``` + +`AddOpcUa()` registers a `ServiceProviderTelemetryContext` that adapts +the host's `ILoggerFactory` to `ITelemetryContext` — no separate logging +pipeline is required. `IOpcUaServerBuilder.AddNodeManager()` registers +an `IAsyncNodeManagerFactory`; use `AddSyncNodeManager()` for the +legacy `INodeManagerFactory`. For advanced configuration (custom security +policies, additional builder calls), set `OpcUaServerOptions.ConfigureBuilder`. + +That's the whole server. The Boiler version is in +`samples/MinimalApi/MinimalBoilerServer/Program.cs`. + +> `AutoAcceptUntrustedCertificates` is enabled here only for a local sample. +> Configure trusted certificates and disable automatic acceptance in production; +> see [Certificates](Certificates.md). + ## Built-in node managers Replicated application managers require a fixed @@ -218,41 +268,43 @@ The default server does not create file-system, alias-name, or runtime-NodeSet m ## Core vs custom node managers -This document outlines the key differences in behavior and implementation between `CoreNodeManager` and `CustomNodeManager2` within the OPC UA .NET Standard Stack. - -`CoreNodeManager` is typically used for managing the internal nodes of the Server (Namespace 0) or simple static node sets. `CustomNodeManager2` is designed as a base class for developers implementing custom node managers with specific business logic, dynamic behavior, or backing stores. +`CoreNodeManager` is the built-in owner of core server nodes. It derives from +`AsyncCustomNodeManager` and therefore shares its NodeState-based service, +monitoring, and synchronization infrastructure. For application models, prefer +source-generated managers or an application-owned `AsyncCustomNodeManager`; +`CustomNodeManager2` remains the synchronous compatibility base. ### Storage and data structures -| Feature | CoreNodeManager | CustomNodeManager2 | +| Concern | CoreNodeManager / AsyncCustomNodeManager | CustomNodeManager2 | | :--- | :--- | :--- | -| **Node Storage** | Uses a `NodeTable` (`m_nodes`) internally. | Uses a `NodeIdDictionary` (`PredefinedNodes`). | -| **Node Type** | Manages `ILocalNode` interface objects. | Manages `NodeState` objects (and subclasses). | -| **Handle Type** | `GetManagerHandle` returns the `ILocalNode` instance directly. | `GetManagerHandle` returns a `NodeHandle` wrapper containing the `NodeState` and validation status. | -| **Locking** | Uses `DataLock` (object). | Uses `Lock` (object). | -| **Namespace** | Typically manages dynamic nodes in specific indexes or internal server nodes. | Designed to manage specific namespaces passed in the constructor. Uses `IsNodeIdInNamespace` checks. | +| Nodes | `NodeState` instances indexed by `PredefinedNodes` | `NodeState` instances indexed by `PredefinedNodes` | +| Service handles | Validated `NodeHandle` objects | Validated `NodeHandle` objects | +| Namespace ownership | Core owns built-in routes; application managers declare their namespaces | Application managers declare their namespaces | +| Synchronization | Awaitable manager operations and privately synchronized node state | Synchronous manager operations and privately synchronized node state | + +Neither design exposes a node's internal lock. Follow the +[threading contract](#threading-contract-for-nodes-and-browsers), not an external +`lock (node)`. ### Extensibility -| Feature | CoreNodeManager | CustomNodeManager2 | -| :--- | :--- | :--- | -| **Design Intent** | Sealed-like behavior. Not primarily designed for inheritance or overriding behavior. | Highly extensible. Most methods (`Read`, `Write`, `Browse`, `Call`) are `virtual` to allow custom overrides. | -| **Node Factory** | Does not implement `INodeIdFactory`. | Implements `INodeIdFactory` to generate new NodeIds for the system context. | -| **Address Space** | `CreateAddressSpace` is often empty (`ImportNodes` is used instead). | `CreateAddressSpace` invokes `LoadPredefinedNodes` to load nodes from resources/assemblies. | +Use the generated `Configure` partial and fluent callbacks for model behavior. +For specialized services, use `AsyncCustomNodeManager`'s asynchronous extension +points. The core manager is server infrastructure, not the preferred home for +application business logic. Its `ImportNodesAsync` adds predefined nodes and +updates diagnostics where required. ### Operational behaviour #### Reading & Writing -* **CoreNodeManager**: - * **Read**: Directly invokes `ILocalNode.Read`. - * **Write**: Performs basic type checking (expected data type/value rank) and invokes `ILocalNode.Write`. -* **CustomNodeManager2**: - * **Read**: Validates the node handle, supports operation caching, and invokes `NodeState.ReadAttribute`. Handles timestamp synchronization (e.g., matching ServerTimestamp to SourceTimestamp for Value attributes). - * **Write**: - * Performs **Range Checks** for `AnalogItemState` (InstrumentRange). - * Generates **Audit Events** (`Server.ReportAuditWriteUpdateEvent`). - * Detects **Semantic Changes** (e.g., changes to `EURange`, `EnumStrings`) and updates monitored items accordingly. +The async manager validates ownership and delegates attribute access to +`NodeState.ReadAttributeAsync` / `WriteAttributeAsync`. Core inherits that path. +The synchronous compatibility manager uses the corresponding synchronous +attribute operations. Type/rank checks, access checks, audit reporting, and +semantic-change notifications remain part of the manager service path; directly +mutating a node value is not a replacement for a client Write service call. #### Method Calls @@ -267,12 +319,12 @@ The awaitable path completes only after that work finishes. Both paths share node ownership, method resolution, role-permission checks, and argument-result handling. -* **CoreNodeManager**: - * **Browse**: Iterates over references stored in `ILocalNode`. Basic masking and filtering. - * **Translate**: Basic search through internal references. -* **CustomNodeManager2**: - * **Browse**: Uses `NodeState.CreateBrowser`. Explicitly validates `PermissionType.Browse`. Supports Views (`IsNodeInView`). - * **Translate**: Uses `CreateBrowser` to navigate path. Supports resolving targets in other node managers via `unresolvedTargetIds`. +#### Browsing and path translation + +Both manager bases browse through `NodeState.CreateBrowser`, validate browse +permissions, and participate in cross-manager path resolution. The async manager +uses `INodeBrowser.NextAsync` for iteration; see the +[browser contract](#threading-contract-for-nodes-and-browsers). Browse continuation ownership transfers explicitly between the dispatcher, node manager, and session store. Permission rejection, metadata/fetch @@ -327,11 +379,12 @@ The operation is deliberately exposed as a capability interface method rather th ### Monitoring and subscriptions -| Feature | CoreNodeManager | CustomNodeManager2 | -| :--- | :--- | :--- | -| **Manager** | Uses `SamplingGroupManager` directly. | Uses `IMonitoredItemManager` abstraction (defaults to `SamplingGroupMonitoredItemManager` or `MonitoredNodeMonitoredItemManager`). | -| **Filter Validation** | Validates `DataChangeFilter` specifically (deadband, EU Range). | Delegates validation to `ValidateMonitoringFilter`, supports `AggregateFilter` (if supported by server) and `DataChangeFilter`. | -| **Events** | Basic event subscription support (`SubscribeToEvents` checks `EventNotifier` bit). | **Full Event Support**:
- Manages `RootNotifiers`.
- Propagates events via `SubscribeToAllEvents`.
- Implements `ConditionRefresh`.
- Validates `PermissionType.ReceiveEvents`. | +Core uses the async manager's `IMonitoredItemManager` infrastructure with +sampling groups enabled. Application `AsyncCustomNodeManager` instances can use +change-triggered monitoring or opt into sampling groups. Both manager bases +support filter validation, event subscriptions, condition refresh, and role checks. +See [monitoring selection](AsyncServerSupport.md#monitored-item-manager-selection) +for the tradeoff between periodic sampling and change notifications. #### Sampling interval revision @@ -394,21 +447,18 @@ managers that implement `INodeManager` directly can call ### History -* **CoreNodeManager**: - * `HistoryRead` / `HistoryUpdate`: Iterates nodes and returns `BadNotReadable` / `BadNotWritable` (or `BadHistoryOperationUnsupported` implicit). No infrastructure for history. -* **CustomNodeManager2**: - * Provides scaffold methods (`HistoryReadRawModified`, `HistoryReadProcessed`, `HistoryUpdateData`, etc.). - * Checks `AccessLevels.HistoryRead/Write` and `EventNotifier.HistoryRead/Write`. - * Default implementation returns `BadHistoryOperationUnsupported`, but is structured for easy overriding in derived classes. +Core inherits the async manager's history dispatch rather than maintaining a +separate `ILocalNode` history path. Advertising history access does not itself +provide storage: configure the appropriate historian provider and node handlers. +See [Historical Access](HistoricalAccess.md) for provider contracts, read/update +operations, and capability advertisement. ### Security -* **CoreNodeManager**: - * Checks `AccessLevel`, `UserAccessLevel`, `WriteMask` in `Write`. - * Loads Role Permissions into metadata. -* **CustomNodeManager2**: - * Explicitly calls `MasterNodeManager.ValidateRolePermissions` during `Browse`, `Call`, and Event processing. - * Reads and caches validation attributes (`AccessRestrictions`, `RolePermissions`) for optimized access. +Both manager bases participate in access-level, attribute-write, and role-permission +checks. Core uses the async base's implementation. Application callbacks must +preserve those service checks and enforce any additional data-source permissions. +See [role-based user management](RoleBasedUserManagement.md). ### Threading contract for nodes and browsers @@ -495,6 +545,35 @@ Three rules follow for node types that customise browsing: `PopulateBrowser` directly leaves construction unserialized against other browses of the same node and skips `OnPopulateBrowser` altogether. +### Delivery and callback lifecycle + +Event delivery for a node follows enqueue order. The Server node and +nodes opting into parallel event consumers deliver each event concurrently +to independent monitored items, then complete that event before advancing +the shared queue. A slow permission check cannot let a later event overtake +an earlier one; the bounded queue still applies backpressure. + +Monitored-item deletion and mode changes isolate failures by owner. +Already completed results are retained, remaining items for a failed +owner receive an explicit error, and other owners still run. Event +unsubscribe failures likewise do not skip later owners or monitored items. +Request cancellation continues to stop the operation rather than being +converted into an ordinary per-item failure. + +Fluent node builders reject new handler registration once graph authoring +is sealed, including callbacks wired directly to node slots. History, +removal, condition-refresh, and monitored-item callbacks on ad-hoc child +and instance builders use the same dispatcher as regular node builders. +A manager without that dispatcher rejects those registrations explicitly. + +Monitored-source lifecycle ownership is rechecked after a worker stops. +A superseded deactivate/reactivate cycle keeps its live source rather than +reacquiring it and releasing the replacement. Callback invocation is +coordinated without holding a gate across its asynchronous completion, so +first-subscriber callbacks can still reenter monitoring operations. +Virtual monitored sources register the same shutdown behavior as concrete +sources; every live materialized instance is released at teardown. + ## Registering node managers A NodeManager owns a part of the server address space. This section explains the three points at @@ -666,7 +745,8 @@ preserves old subscriptions without migration, and immediate reload fails old su #### MonitoredItems -Active MonitoredItems survive reload and removal. A compatible NodeId in a replacement generation +For normal reload, active MonitoredItems are migrated; removal leaves them detached. +A compatible NodeId in a replacement generation keeps the same MonitoredItem and Subscription without a transient bad status. A removed or incompatible NodeId is detached and publishes one `BadNodeIdUnknown` data-change notification, as required by OPC UA Part 4 §5.8.4.1; adding a compatible Node with the same NodeId later revalidates @@ -702,9 +782,11 @@ custom Subscription implementation needs the equivalent snapshots from #### Continuation points -Reload and removal invalidate saved Browse continuation points owned by the retired NodeManager. A -later `BrowseNext` with one of those tokens returns `BadContinuationPointInvalid` instead of -invoking a disposed generation. +Normal reload, immediate reload, and removal invalidate retired-generation Browse +continuation points after captured requests drain. Shadow reload retains them while +the old generation remains active for its monitored items, then invalidates them at +cleanup. A later `BrowseNext` on an invalidated token returns +`BadContinuationPointInvalid` rather than invoking a disposed generation. #### Namespaces @@ -1741,6 +1823,12 @@ This means **instance methods imported from a NodeSet2** (whose `InputArguments`/`OutputArguments` live on the referenced declaration) get the same typed `OnCall` overloads as methods authored in a ModelDesign. +A typed `{Name}MethodState` class is generated only for a method type, such +as `AddMethodType` in the calculator sample's model. A ModelDesign method that +declares its arguments directly, without a method type, is a plain +`MethodState` node: its `InputArguments` and `OutputArguments` properties and +its typed `OnCall` overloads are generated in the same way. + ```csharp [NodeManager(NamespaceUri = "http://opcfoundation.org/UA/Calc/")] public partial class CalcNodeManager @@ -1954,46 +2042,6 @@ real client `MonitoredItem` with an `EventFilter` and asserts the heartbeats arrive end-to-end under NativeAOT constraints (no JIT, no reflection). -### Single-file `Program.cs` — what it looks like - -The shipping `services.AddOpcUa().AddServer(...)` extension wires the -server into the .NET Generic Host: configuration, certificate check, -`ApplicationInstance` lifetime and Ctrl+C/SIGTERM handling are all owned -by the host. User code stays at ~12 lines. - -```csharp -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); -builder.Logging.AddConsole(); - -builder.Services - .AddOpcUa() - .AddServer(o => - { - o.ApplicationName = "MyServer"; - o.ApplicationUri = "urn:localhost:MyServer"; - o.ProductUri = "uri:opcfoundation.org:MyServer"; - o.AutoAcceptUntrustedCertificates = true; - o.EndpointUrls.Add("opc.tcp://localhost:51210/MyServer"); - }) - .AddNodeManager(); - -await builder.Build().RunAsync(); -``` - -`AddOpcUa()` registers a `ServiceProviderTelemetryContext` that adapts -the host's `ILoggerFactory` to `ITelemetryContext` — no separate logging -pipeline is required. `IOpcUaServerBuilder.AddNodeManager()` registers -an `IAsyncNodeManagerFactory`; use `AddSyncNodeManager()` for the -legacy `INodeManagerFactory`. For advanced configuration (custom security -policies, additional builder calls), set `OpcUaServerOptions.ConfigureBuilder`. - -That's the whole server. The Boiler version is in -`samples/MinimalApi/MinimalBoilerServer/Program.cs`. - ### Multi-namespace and manager-swap subclassing Because the generated factory members are `virtual`, you can extend @@ -2715,7 +2763,7 @@ the state NodeId; migrating an override written against 1.5.378 is covered by th - `samples/MinimalApi/MinimalBoilerServer/` — a fully self-contained, NativeAOT single-file Boiler server. Read it top-to-bottom in - <200 lines. + under 200 lines. - `samples/MinimalApi/MinimalCalcServer/` — a calculator server that exercises the typed [methods-with-arguments OnCall overloads](#methods-with-arguments--typed-oncall-overloads) diff --git a/docs/NodeSetExport.md b/docs/NodeSetExport.md index b9f1fc0cab..9bf016a4b8 100644 --- a/docs/NodeSetExport.md +++ b/docs/NodeSetExport.md @@ -1,6 +1,27 @@ # Client-based NodeSet Export -The OPC UA .NET Standard stack provides the ability to export the address space from an OPC UA server to a NodeSet2 XML file using the client library. This allows you to browse a server's address space and save it to a file that can be used for documentation, analysis, or import into other systems. +The OPC UA .NET Standard client library can export a server's address space +to a NodeSet2 XML file. Use the export to document or analyze an address +space, or to import it into another system. + +## Contents + +- [Overview](#overview) +- [Usage](#usage) + - [Basic Example](#basic-example) + - [Using the Console Reference Client](#using-the-console-reference-client) +- [Supported Node Types](#supported-node-types) +- [Features](#features) + - [Node Attributes Exported](#node-attributes-exported) + - [Namespace Handling](#namespace-handling) + - [References](#references) +- [Limitations](#limitations) +- [API Reference](#api-reference) + - [`CoreClientUtils.ExportNodesToNodeSet2`](#coreclientutilsexportnodestonodeset2) + - [`ClientSamples.ExportNodesToNodeSet2`](#clientsamplesexportnodestonodeset2) +- [Related Topics](#related-topics) +- [Examples](#examples) +- [See Also](#see-also) ## Overview diff --git a/docs/OpenUsd.md b/docs/OpenUsd.md index bb0fcf8337..71fbb5e65e 100644 --- a/docs/OpenUsd.md +++ b/docs/OpenUsd.md @@ -16,6 +16,30 @@ requires the other. | OPC UA carries | *which* prim/attribute a value maps to | the prims and attributes themselves | | Consumer | a connector that writes an external stage | a client that browses/subscribes, or exports `.usda` | +## Contents + +- [Libraries](#libraries) +- [The connector](#the-connector) + - [Progressive API](#progressive-api) + - [Values cross the boundary as `Variant`](#values-cross-the-boundary-as-variant) + - [History replay and commands](#history-replay-and-commands) + - [Integrity](#integrity) +- [Dependency injection](#dependency-injection) +- [Server-side authoring](#server-side-authoring) +- [Samples](#samples) +- [The connector tool](#the-connector-tool) + - [Rendering the twin live](#rendering-the-twin-live) + - [Viewport colour, materials and cameras](#viewport-colour-materials-and-cameras) +- [Part 2 — scene materialization](#part-2--scene-materialization) + - [Materializing a stage](#materializing-a-stage) + - [Nothing is ever dropped](#nothing-is-ever-dropped) + - [Types carry USD roles](#types-carry-usd-roles) + - [Live attributes](#live-attributes) + - [Georeferencing](#georeferencing) + - [Conversion and the round-trip contract](#conversion-and-the-round-trip-contract) + - [Discovery and Part 1 interop](#discovery-and-part-1-interop) + - [Conformance units](#conformance-units) + > Both companion models are **draft** (experimental) models. The type NodeIds and the > `Server/OpenUSD/Representations` registry described here are subject to change until the companion specifications > are ratified. @@ -281,12 +305,10 @@ and the callback fires when that target changes. > RID-less build or publish on a supported host copies that host's OpenUSD native payload. Use an explicit RID when > publishing for another platform. Publish the connector and the viewport into the *same* directory, substituting your > own RID: -> > ``` > dotnet publish tools/Opc.Ua.OpenUsd.Connector -c Release -f net10.0 -r win-x64 --self-contained false -o out > dotnet publish tools/Opc.Ua.OpenUsd.Connector.Viewer -c Release -r win-x64 --self-contained false -o out > ``` -> > Publishing both into the same directory is what puts the optional assembly, its dependencies, and the native > plugin tree where the connector looks for them. diff --git a/docs/Positioning.md b/docs/Positioning.md index fab483b9b8..ee7668bed6 100644 --- a/docs/Positioning.md +++ b/docs/Positioning.md @@ -1,10 +1,23 @@ # OPC UA Relative Spatial Location and Global Positioning -The Positioning libraries implement OPC UA Relative Spatial Location (RSL, -[OPC 10000-210](https://reference.opcfoundation.org/specs/OPC-10000-210)) and -Global Positioning (GPOS, -[OPC 10000-211](https://reference.opcfoundation.org/specs/OPC-10000-211)). -They use the released RSL 1.00.1 and GPOS 1.0.0 NodeSets. GPOS depends on RSL. +The Positioning libraries implement two OPC UA companion specifications: + +- Relative Spatial Location (RSL), defined by + [OPC 10000-210](https://reference.opcfoundation.org/specs/OPC-10000-210). +- Global Positioning (GPOS), defined by + [OPC 10000-211](https://reference.opcfoundation.org/specs/OPC-10000-211). + +The libraries use the released RSL 1.00.1 and GPOS 1.0.0 NodeSets. GPOS +depends on RSL. + +## Contents + +- [Packages](#packages) +- [Minimal standalone server](#minimal-standalone-server) +- [Server authoring](#server-authoring) +- [Client](#client) +- [Transform conventions](#transform-conventions) +- [Robot and OpenUSD sample](#robot-and-openusd-sample) ## Packages @@ -114,18 +127,24 @@ the failure through `ITelemetryContext`, and fault `Completion`. `IGeoLocationProvider` is intentionally technology-neutral, and lives in `Opc.Ua.Server` (namespace `Opc.Ua`) rather than in a companion-model assembly. -GPS/WGS84 is a built-in use case, but RTLS, UWB, RFID, local floor-plan -coordinates, and other tracking systems use the same contract. An RSL provider +Global Positioning System (GPS) and World Geodetic System 1984 (WGS84) +coordinates are built-in use cases. Real-time location systems (RTLS), +ultra-wideband (UWB), radio-frequency identification (RFID), local +floor-plan coordinates, and other tracking systems can use the same contract. +An RSL provider is also useful when a robot controller, metrology system, or kinematic service is authoritative for a relative frame rather than a global coordinate. Because the contract is shared, **one provider implementation serves every model that publishes location**. The same instance can back GPOS `GlobalLocation` Variables here and OPC 10030 (ISA-95) `GeoSpatialLocationType` -Variables — see [ISA-95](ISA95.md#geospatiallocationtype-provider-seam). A -sample carries an optional `GeoPosition` (latitude, longitude, optional height, -accuracy, floor and EPSG code), an optional `GeoOrientation`, and optional text -`Labels`: +Variables — see [ISA-95](ISA95.md#geospatiallocationtype-provider-seam). +A sample can include: + +- `GeoPosition` with latitude, longitude, optional height, accuracy, floor, + and EPSG code +- `GeoOrientation` +- Text labels ```csharp public sealed class MyGpsProvider : IGeoLocationProvider @@ -154,11 +173,12 @@ Set `SupportsPush` to `false` when the source can only be polled; the binding layer then never calls `WatchAsync` and reads through `ReadAsync` at the Variable's sampling interval instead. -A GPOS Variable requires coordinates, so a sample whose `Position` is `null` -is rejected with `BadNoDataAvailable`. When a position declares an `EpsgCode` -that differs from the Variable's configured `CoordinateReferenceSystem`, the -binding fails rather than silently mis-georeferencing the value; leave -`EpsgCode` `null` to accept whatever the Variable is configured for. +A GPOS Variable requires coordinates. If a sample has `Position` set to +`null`, the server rejects it with `BadNoDataAvailable`. If the position's +`EpsgCode` differs from the Variable's configured +`CoordinateReferenceSystem`, the binding fails rather than mis-georeference +the value silently. Set `EpsgCode` to `null` to accept the Variable's +configured coordinate reference system. `InMemoryGeoLocationProvider` ships as a reference implementation for tests and for servers whose positions are pushed in from elsewhere. Watching an unknown source does not create a sample: reads return `BadNotFound` until `Update` @@ -225,11 +245,11 @@ The built-in coordinate-reference-system transformer supports WGS84 / EPSG:4326, ECEF, and local East-North-Up coordinates. Inject `ICoordinateReferenceSystemTransformer` for another CRS. -`GroundControlPointFitter` supports rigid, similarity, and affine fits. It -selects a horizontal 2D fit when elevation or rank is insufficient, forces a -proper rotation for rigid/similarity modes, rejects affine reflections unless -enabled, and reports residual, determinant, rank, dimension, and invertibility -diagnostics. +`GroundControlPointFitter` supports rigid, similarity, and affine fits. When +elevation or rank is insufficient, it selects a horizontal 2D fit. It forces +a proper rotation for rigid and similarity modes, and rejects affine +reflections unless you enable them. The fitter reports residual, determinant, +rank, dimension, and invertibility diagnostics. ## Robot and OpenUSD sample diff --git a/docs/ProductionChecklist.md b/docs/ProductionChecklist.md new file mode 100644 index 0000000000..71af210529 --- /dev/null +++ b/docs/ProductionChecklist.md @@ -0,0 +1,123 @@ +# Production readiness checklist + +The samples and [Getting started](GettingStarted.md) use shortcuts that are +suitable only on a development computer: temporary certificate stores, +automatic certificate acceptance, anonymous users, and short reconnect limits. +Review this checklist before you deploy a client or server. Each item links to +the guide that explains the setting. The checklist does not replace a security +review of your deployment. + +## Contents + +- [Application identity and certificates](#application-identity-and-certificates) +- [Endpoints and message security](#endpoints-and-message-security) +- [Users and permissions](#users-and-permissions) +- [Secrets and private keys](#secrets-and-private-keys) +- [Resource limits](#resource-limits) +- [Diagnostics and auditing](#diagnostics-and-auditing) +- [Clients](#clients) +- [Deployment and operation](#deployment-and-operation) + +## Application identity and certificates + +- [ ] Give each installation a unique `ApplicationUri` that matches its + application certificate. See [Concepts: applications](Concepts.md#applications). +- [ ] Set `PkiRoot` to a persistent, access-controlled directory. The hosted + client and server default to a folder in the system temporary directory. See + [Application identity and certificate defaults](DependencyInjection.md#application-identity-and-certificate-defaults). +- [ ] Keep `AutoAcceptUntrustedCertificates` disabled, and provision the + trusted and issuer certificates, or CA-issued certificates, before peers + connect. See [Certificates](Certificates.md). +- [ ] Keep SHA-1 certificates rejected and an RSA key size of at least 2048 + bits; both are the defaults. See + [Server security and resource controls](DependencyInjection.md#server-security-and-resource-controls). +- [ ] Plan certificate renewal and trust-list updates, for example with + certificate-expiration alarms, push management, or a Global Discovery Server. + See [Certificate manager](CertificateManager.md) and [GDS](GDS.md). + +## Endpoints and message security + +- [ ] Offer `SignAndEncrypt` endpoints and keep `IncludeUnsecurePolicyNone` + set to `false`; both are the hosted-server defaults. See + [Server security and resource controls](DependencyInjection.md#server-security-and-resource-controls). +- [ ] Enable `IncludeEccPolicies` only when every client supports the ECC + policies. ECC SecureChannel policies require the .NET 8 or later assemblies. + See [ECC profiles: known limitations](EccProfiles.md#known-limitations). +- [ ] Configure clients to select a `SignAndEncrypt` endpoint. See + [Concepts: endpoints, secure channels, and sessions](Concepts.md#endpoints-secure-channels-and-sessions). + +## Users and permissions + +- [ ] Decide whether anonymous access is acceptable. A hosted server without + `UserTokenPolicies` advertises an `Anonymous` policy. See + [User token policies](DependencyInjection.md#user-token-policies). +- [ ] Register an authenticator for each user-token policy that the server + advertises, for example with `AddDefaultIdentityAuthenticators`. A policy + without an authenticator does not authenticate users. See + [Identity providers](IdentityProviders.md). +- [ ] Map identities to roles and restrict node permissions. See + [Role-based user management](RoleBasedUserManagement.md). +- [ ] Set session and channel ceilings for the expected load, and keep the + failed-authentication lockout (`MaxFailedAuthenticationAttempts`, default 5). + See [Server security and resource controls](DependencyInjection.md#server-security-and-resource-controls). + +## Secrets and private keys + +- [ ] Keep passwords, tokens, and private keys out of source code, + configuration files in source control, and container images. +- [ ] Store credentials through an `ISecretStore` that is backed by a durable, + access-controlled secret service; a hosted server registers one with + `AddSecretStore`. See [Dependency injection: server feature](DependencyInjection.md#server-feature). +- [ ] Where policy requires it, keep private keys in a TPM, HSM, PKCS#11 token, + or key service. See [Crypto providers](CryptoProvider.md). + +## Resource limits + +- [ ] Review the transport quotas (`MaxMessageSize`, `MaxArrayLength`, + `MaxByteStringLength`) and the service `OperationLimits`. See + [Operation limits](DependencyInjection.md#operation-limits). +- [ ] Tune the admission rate limits, which are enabled by default, with + `ConfigureRateLimits`. See [Rate limiting](RateLimiting.md). +- [ ] Review the resource-isolation profile; `Balanced` is the default. See + [Server resource isolation](ResourceIsolation.md). +- [ ] Size the server for the expected sessions, subscriptions, and monitored + items. See [Server scalability](ServerScalability.md). + +## Diagnostics and auditing + +- [ ] Send logs, metrics, and traces to your monitoring system through the + host's logging and `ITelemetryContext`. See [Diagnostics](Diagnostics.md). +- [ ] Enable audit events when you must record security-relevant actions; they + are off by default. See [Enabling auditing](Diagnostics.md#enabling-auditing). +- [ ] Decide whether to keep the server diagnostics nodes (`DiagnosticsEnabled`, + default `true`), which add per-session and per-subscription work. See + [Enabling diagnostics](Diagnostics.md#enabling-diagnostics). +- [ ] Use packet capture and key logs only for troubleshooting. Key logs make + captured traffic decryptable. See [Diagnostics: security model](Diagnostics.md#security-model). + +## Clients + +- [ ] Keep the default reconnect policy for long-running clients. The Getting + started client limits retries only to report errors quickly. See + [Reconnect semantics](Sessions.md#reconnect-semantics-on-managedsession). +- [ ] Trust server certificates explicitly instead of accepting them + automatically. See [Certificates](Certificates.md). +- [ ] Store namespace URIs, not namespace indexes, and resolve indexes after + each connection. See [Concepts: node identity and namespaces](Concepts.md#node-identity-and-namespaces). +- [ ] Pass cancellation tokens and dispose sessions and subscriptions. See + [Concepts: how the SDK is organized](Concepts.md#how-the-sdk-is-organized). + +## Deployment and operation + +- [ ] Keep the clocks of clients and servers synchronized, for example with + NTP or PTP. Certificate validity, timestamps, and redundancy depend on it. + See [Kubernetes: time synchronization](Kubernetes.md#time-synchronization). +- [ ] In containers, mount the certificate stores on persistent storage and + supply secrets at run time. See + [Container reference server](ContainerReferenceServer.md) and + [Kubernetes: secrets, certificates, and trust](Kubernetes.md#secrets-certificates-and-trust). +- [ ] Use a Global Discovery Server or provisioning mode to manage certificates + across many installations. See [GDS](GDS.md) and + [Provisioning mode](ProvisioningMode.md). +- [ ] Add redundancy only when availability requirements need it. See + [High availability](HighAvailability.md). diff --git a/docs/Profiles.md b/docs/Profiles.md index db3411b73c..96d72157d9 100644 --- a/docs/Profiles.md +++ b/docs/Profiles.md @@ -9,12 +9,21 @@ The OPC UA .NET Standard Stack is a reference implementation that targets compliance through an OPC Foundation Certification Test Lab and is continuously tested for compliance using the latest Compliance Test Tool (CTT). -The stack ships full server- and client-side support for: Part 9 (Alarms & -Conditions), Part 11 (Historical Access) + Part 13 (Aggregates), Part 16 -(State Machines), Part 17 (Alias Names), Part 18 (Role Management), Part 20 -(File Transfer), Part 100 (Device Integration / Software Update), OPC 10100-1 -(WoT Connectivity), OPC 40001-1 (Industrial Automation), OPC 40010-1 -(Robotics), and the Local Discovery Server. See +The stack ships server- and client-side support for: + +- Part 9 (Alarms & Conditions) +- Part 11 (Historical Access) and Part 13 (Aggregates) +- Part 16 (State Machines) +- Part 17 (Alias Names) +- Part 18 (Role Management) +- Part 20 (File Transfer) +- Part 100 (Device Integration / Software Update) +- OPC 10100-1 (WoT Connectivity) +- OPC 40001-1 (Industrial Automation) +- OPC 40010-1 (Robotics) +- The Local Discovery Server + +See [What's New in 2.0](WhatsNewIn2.0.md) for the broader change narrative. The canonical list of all OPC UA profile and facet URIs is maintained by the @@ -24,6 +33,41 @@ advertises in `ServerProfileArray`; URIs not yet present in a shipping config are referred to *by name* and the reader should consult the OPC Foundation registry for the canonical URI form. +## Contents + +- [Overview](#overview) +- [Server Profiles](#server-profiles) + - [Core Server Profiles](#core-server-profiles) + - [Functional Facets](#functional-facets) + - [Local Discovery Server (LDS) Profile](#local-discovery-server-lds-profile) + - [Additional facets supported by the implementation (beyond the default advertised set)](#additional-facets-supported-by-the-implementation-beyond-the-default-advertised-set) +- [Client Profiles](#client-profiles) +- [Transport Profiles](#transport-profiles) + - [Client and server transports](#client-and-server-transports) + - [PubSub transports](#pubsub-transports) +- [Security Profiles](#security-profiles) + - [RSA-based security policies](#rsa-based-security-policies) + - [ECC-based security policies](#ecc-based-security-policies) + - [Traditional ECC curves](#traditional-ecc-curves) + - [Modern ECC curves (v2.0)](#modern-ecc-curves-v20) + - [AES-GCM and ChaCha20-Poly1305 variants (v2.0)](#aes-gcm-and-chacha20-poly1305-variants-v20) + - [RSA Diffie-Hellman (v2.0)](#rsa-diffie-hellman-v20) + - [Deprecated security policies](#deprecated-security-policies) + - [Security policy None](#security-policy-none) +- [User Authentication](#user-authentication) +- [Certificate Types](#certificate-types) + - [RSA certificates](#rsa-certificates) + - [ECC certificates](#ecc-certificates) +- [Global Discovery Server (GDS)](#global-discovery-server-gds) +- [Message Encoding](#message-encoding) +- [Specification Compliance](#specification-compliance) +- [Configuration](#configuration) + - [Server profile configuration](#server-profile-configuration) + - [Security policy configuration](#security-policy-configuration) +- [Related Documentation](#related-documentation) + - [Core and companion spec related documentation](#core-and-companion-spec-related-documentation) +- [References](#references) + ## Server Profiles The reference server (`samples/Reference/ConsoleReferenceServer`) advertises the @@ -361,12 +405,15 @@ Modern AEAD cipher alternatives for traditional ECC curves: - **RSA_DH_AesGcm** — RSA Diffie-Hellman key agreement with AES-GCM - **RSA_DH_ChaChaPoly** — RSA Diffie-Hellman key agreement with ChaCha20-Poly1305 -**Platform requirements for ECC.** ECC support is available on .NET -Framework 4.8, .NET Standard 2.1, and .NET 5.0 or later. Modern curves -(Curve25519, Curve448) and AEAD ciphers (AES-GCM, ChaCha20-Poly1305) -require .NET 8.0 or later (`AesGcm.IsSupported` / -`ChaCha20Poly1305.IsSupported` guard the runtime registration). Not all -curves are supported by every OS platform and .NET implementation. +**Platform requirements for ECC.** Certificate creation, storage, and validation +are distinct from ECC SecureChannel and user-token security policies. Those policies +require the stack's .NET 8.0-or-later target assets; running a .NET Standard asset +on a newer runtime does not add the raw-secret APIs excluded at build time. +Curve and cipher availability also depends on the OS and runtime probes +(`AesGcm.IsSupported` / `ChaCha20Poly1305.IsSupported`). See the +[ECC platform limitations](EccProfiles.md#known-limitations) for the certificate-operation +and channel-policy boundaries. PubSub has its own +[transport security limits](PubSub.md#security). ### Deprecated security policies @@ -426,8 +473,8 @@ for storage, ref-counted lifetime, and the segregated-interface design. ## Global Discovery Server (GDS) -The stack ships a Global Discovery Server implementation that is -**full OPC UA Part 12 compliance**, including: +The stack ships a Global Discovery Server implementation covering the following +OPC UA Part 12 areas: - Application registration and discovery. - Pull and Push certificate-management models, including pushing to @@ -442,8 +489,9 @@ The stack ships a Global Discovery Server implementation that is Credential issuance for non-OPC UA services such as MQTT brokers and REST APIs, backed by `IKeyCredentialRequestStore` / `ISecretStore`. -See the [GDS Developer Guide](GDS.md) for the full feature breakdown and -hosting integration. The Local Discovery Server is a separate library +See the [GDS Developer Guide](GDS.md) for the implemented facets, provider-dependent +features, partial support, and hosting integration. This is not a claim that every +optional Part 12 function is implemented or certified. The Local Discovery Server is a separate library (`Opc.Ua.Lds.Server`) and reference application (`ConsoleLdsServer`) that advertises the [Local Discovery Server 2017](http://opcfoundation.org/UA-Profile/Server/LocalDiscovery2017) @@ -469,10 +517,12 @@ server-defined types. ## Specification Compliance - **OPC UA Specification:** Version 1.05.07. -- **Certification:** The reference server has been certified for - compliance through an OPC Foundation Certification Test Lab. -- **Testing:** All releases are verified for compliance using the latest - Compliance Test Tool (CTT). +- **Certification:** Historical reference-server certification does not establish + certification of the current release or of an application built with the SDK. + Verify the certificate and tested profile scope for the exact product version. +- **Testing:** Automated repository tests provide implementation evidence, not a + substitute for certification. The [GDS conformance matrix](GDS.md) + explicitly distinguishes its test evidence from CTT results. - **Redundancy (Part 4 §6.6):** Server and client redundancy are implemented (opt-in) — `RedundancySupport`, `Server.ServiceLevel`, non-transparent (`ServerUriArray` / `RedundantServerArray`) and transparent (`CurrentServerId`) diff --git a/docs/ProvisioningMode.md b/docs/ProvisioningMode.md index 8c1c3f40fb..38a01d5ad5 100644 --- a/docs/ProvisioningMode.md +++ b/docs/ProvisioningMode.md @@ -4,6 +4,21 @@ Provisioning mode is a special server startup mode designed to facilitate secure certificate provisioning and initial server configuration. When enabled, the server starts with a limited default namespace and enhanced security settings to allow administrators to safely configure the server before exposing the full application functionality. +## Contents + +- [Overview](#overview) +- [Purpose](#purpose) +- [How to Enable Provisioning Mode](#how-to-enable-provisioning-mode) + - [ConsoleReferenceServer](#consolereferenceserver) + - [Command Line Options](#command-line-options) +- [Behavior in Provisioning Mode](#behavior-in-provisioning-mode) + - [Limited Namespace](#1-limited-namespace) + - [Authentication Requirements](#2-authentication-requirements) + - [Certificate Handling](#3-certificate-handling) + - [Reduced Attack Surface](#4-reduced-attack-surface) +- [Use Cases](#use-cases) + - [Initial Server Setup](#initial-server-setup) + ## Purpose The provisioning mode addresses the need for secure server initialization by: diff --git a/docs/PubSub.md b/docs/PubSub.md index 88c352445a..ae82941f88 100644 --- a/docs/PubSub.md +++ b/docs/PubSub.md @@ -6,10 +6,16 @@ > understands the OPC UA PubSub model > ([Part 14 §4](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/4)) > and focuses on **how to use the library**. +> +> New to PubSub? [OPC UA concepts](Concepts.md#clientserver-and-pubsub) +> introduces publishers, subscribers, and datasets. +> [Run a first publisher and subscriber](#run-a-first-publisher-and-subscriber) +> exchanges messages on one computer with the reference sample. ## Table of contents - [At a glance](#at-a-glance) +- [Run a first publisher and subscriber](#run-a-first-publisher-and-subscriber) - [Architecture](#architecture) - [Core abstractions](#core-abstractions) - [Fluent builder walkthrough](#fluent-builder-walkthrough) @@ -18,15 +24,15 @@ - [Encodings](#encodings) - [Transcoding](#transcoding) - [Discovery](#discovery) -- [Security](#security) +- [Security](#security-1) - [Security Key Service (SKS)](#security-key-service-sks) +- [Actions (request/response)](#actions-requestresponse) - [Server-side address space](#server-side-address-space) - [Binding PubSub to an external OPC UA server (client-session adapters)](#binding-pubsub-to-an-external-opc-ua-server-client-session-adapters) - [High availability and redundancy](#high-availability-and-redundancy) - [Diagnostics](#diagnostics) - [Native AOT](#native-aot) - [Spec coverage](#spec-coverage) -- [Test coverage](#test-coverage) - [Cross-references](#cross-references) ## At a glance @@ -59,6 +65,72 @@ `IPubSubApplication.AddConnectionAsync` / `AddWriterGroupAsync` / etc. - High-performance transcoders bridge subscriber-side NetworkMessages to publisher connections with UADP/JSON cross-encoding, transform pipelines, and managed UADP re-securing. +## Run a first publisher and subscriber + +The [reference PubSub sample](../samples/PubSub/ConsoleReferencePubSubClient/README.md) +contains a publisher and a subscriber in one executable. This exchange runs on +one computer and needs no broker. From the repository root, build the sample +once: + +```bash +dotnet build samples/PubSub/ConsoleReferencePubSubClient +``` + +Start the subscriber in one terminal: + +```bash +dotnet run --no-build --project samples/PubSub/ConsoleReferencePubSubClient -- subscriber --profile udp-uadp --endpoint opc.udp://127.0.0.1:4840 +``` + +Start the publisher in a second terminal: + +```bash +dotnet run --no-build --project samples/PubSub/ConsoleReferencePubSubClient -- publisher --profile udp-uadp --endpoint opc.udp://127.0.0.1:4840 +``` + +The subscriber logs each DataSet it decodes, once per second: + +```text +DataSet #1 received (3 fields): BoolToggle=False, Int32=1, DateTime=09/28/2026 12:18:50 +DataSet #2 received (3 fields): BoolToggle=True, Int32=2, DateTime=09/28/2026 12:18:51 +``` + +The `udp-uadp` profile signs and encrypts the UADP messages with demo keys +that are built into the sample. A production deployment obtains its keys from +a [Security Key Service](#security-key-service-sks). Two warnings are expected: + +- `DataSetReader Reader 1 faulted on MessageReceiveTimeout (>00:00:05)` + appears when no matching message arrives for five seconds, for example + before the publisher starts. The reader resumes when messages arrive. +- `Dropping unsecured inbound frame ... requiring SignAndEncrypt` appears once. + The publisher announces its DataSet metadata without message security, and + the secured subscriber discards the announcement. It decodes the DataSets + with the metadata configured on its DataSetReader instead. + +The sample's default endpoint, `opc.udp://239.0.0.1:4840`, is a multicast +address. On one computer, the subscriber can miss multicast messages: the UDP +transport disables multicast loopback by default +(`UdpTransportOptions.MulticastLoopback`), and the publisher sends through the +operating system's default multicast interface, which can differ from the +interface that the subscriber joins. The unicast loopback address avoids both +limitations. See [UDP / UADP](#udp--uadp) for the multicast options. + +A subscriber decodes a message only when it matches the publisher's settings: + +| Setting | Publisher option | Subscriber option | Sample default | +| --- | --- | --- | --- | +| Transport, encoding, and security | `--profile` | `--profile` | `udp-uadp` | +| Endpoint | `--endpoint` | `--endpoint` | `opc.udp://239.0.0.1:4840` | +| PublisherId | `--publisher-id` | `--publisher-id-filter` | `1` | +| WriterGroupId | `--writer-group-id` | `--writer-group-id-filter` | `100` | +| DataSetWriterId | `--data-set-writer-id` | `--data-set-writer-id-filter` | `1` | + +The DataSet fields must also agree. The sample uses RawData field encoding, +which carries no type information, so the subscriber decodes the fields with +the `DataSetMetaData` of its DataSetReader: `BoolToggle`, `Int32`, and +`DateTime`. A subscriber whose settings do not match ignores the messages and +reports the `MessageReceiveTimeout` fault. + ## Architecture The library is laid out as five sibling assemblies — the abstractions @@ -219,6 +291,16 @@ on the well-known `ua-metadata` topic at startup and after each configuration version bump; subscribers cache it before the first KeyFrame arrives. +A subscriber also registers the `DataSetMetaData` configured on each +DataSetReader whose filter names an exact PublisherId, WriterGroupId, and +DataSetWriterId +([§6.2.9.4](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.2.9.4)). +It can therefore decode RawData fields without first receiving an +announcement; a later announcement replaces the entry. A reader with a +wildcard filter depends on announcements for RawData. A DataSetMessage that +omits its ConfigurationVersion is decoded with the registered metadata; one +that carries a different MajorVersion is rejected. + Subscribers can also **actively** request discovery information with `IPubSubApplication.RequestDiscoveryAsync(...)` (Part 14 §7.2.4.6): it sends a `UadpDiscoveryRequestMessage` for `DataSetMetaData`, @@ -497,6 +579,20 @@ broadcast. The transport honours the | `MessageRepeatCount` | How many times the publisher re-sends the same NetworkMessage. | | `MessageRepeatDelay` | Delay between repeats; receivers deduplicate using `SequenceNumber`. | +`UdpTransportOptions`, configured with `AddUdpTransport(options => ...)` or the +`OpcUa:PubSub:Udp` configuration section, sets the socket behavior. Its +defaults favor containment over reach: + +| Option | Default | Meaning | +| --- | --- | --- | +| `Ttl` | `1` | Multicast and unicast time-to-live; keeps multicast on the local subnet. | +| `MulticastLoopback` | `false` | Whether this host receives copies of its own multicast messages. | +| `PreferredNetworkInterface` | `null` | NIC name or local IP address used to join multicast groups when the connection address does not name one; otherwise the first operational interface for the address family. | + +A receiving connection joins its multicast group on that interface. A +sending connection uses the operating system's default multicast interface. +For a publisher and subscriber on one computer, use a unicast loopback address, +as in [Run a first publisher and subscriber](#run-a-first-publisher-and-subscriber). ### Ethernet / UADP (`opc.eth://`) @@ -559,7 +655,6 @@ Notes: only UADP encoding is defined for the Ethernet mapping (no JSON over `opc **Security.** The Ethernet mapping provides **no transport-level authentication, integrity, or confidentiality** (unlike `opc.dtls://`): raw Layer 2 frames are unauthenticated and unencrypted, and any node on the broadcast / VLAN domain can sniff, inject, replay, or spoof them. Always configure **message-level PubSub security** (`SecurityMode = SignAndEncrypt` with a SecurityGroup / SKS), exactly as for UDP — the transport applies the same inbound security gate. The transport logs a prominent **warning** when a connection is opened with `SecurityMode = None`. Run the process with the **least privilege** required for raw L2 access — on Linux grant the `CAP_NET_RAW` capability to the binary (`setcap cap_net_raw+ep`) rather than running as root; `Promiscuous` mode is off by default and broadens the receive exposure when enabled. The in-memory loopback backend delivers every frame to all peers on its bus (no destination filtering) and is a test / diagnostic double only — it must not be relied on as a security or isolation boundary. SharpPcap (+ PacketDotNet) are pinned native dependencies tracked under the repository's SDL native-code policy (see `Directory.Packages.props`). - ### DTLS / UADP (`opc.dtls://`) `Opc.Ua.PubSub.Udp` also implements the Part 14 §7.3.2.4 DTLS transport for @@ -650,7 +745,7 @@ transport open throws a clear `NotSupportedException`. | Brainpool P256r1/P384r1 + AES-GCM / ChaCha20 / integrity-only | Implemented only on platforms where the BCL can create the Brainpool curve OID. | Not registered. | None. | | Curve25519 / Curve448 mandatory profiles | Unsupported: .NET BCL has no portable X25519/X448 API; fail-closed. | Unsupported. | Unsupported. | -Peer authentication reuses the injected stack `CertificateValidator` / +Peer authentication reuses the injected stack certificate validator / certificate stores. Certificates must be ECC/ECDSA and match the selected profile hash strength. DTLS records enforce sequence-number protection and anti-replay per RFC 9147. @@ -1913,7 +2008,7 @@ below maps Part 14 sections to the type / file that implements them. - [Migration sub-doc — `migrate/2.0.x/pubsub.md`](migrate/2.0.x/pubsub.md) - [External server adapter](#binding-pubsub-to-an-external-opc-ua-server-client-session-adapters) - [Dependency Injection](DependencyInjection.md) -- [Native AOT Testing](NativeAoT.md) +- [Native AOT](NativeAoT.md) - [Profiles and Facets](Profiles.md#pubsub-transports) - [Certificate Manager](CertificateManager.md) - [Sessions](Sessions.md) — Part 4 service set used by the SKS client. diff --git a/docs/PubSubHighAvailability.md b/docs/PubSubHighAvailability.md index ff83d80d99..90bb2410a7 100644 --- a/docs/PubSubHighAvailability.md +++ b/docs/PubSubHighAvailability.md @@ -1,9 +1,26 @@ # PubSub High Availability -This guide maps OPC UA Part 14 §9.1.6 PubSub redundancy to the OPC UA .NET Standard stack's distributed PubSub high-availability seams. It is the PubSub counterpart to [High Availability and OPC UA Redundancy](HighAvailability.md), which covers OPC 10000-4 §6.6 server, client, subscription, session, and network redundancy. For the base publisher/subscriber runtime, transports, encodings, discovery, diagnostics, and SKS concepts, see [Part 14 PubSub](PubSub.md). +This guide explains how the OPC UA .NET Standard stack implements +distributed PubSub high availability under OPC UA Part 14 §9.1.6. It +complements [High Availability and OPC UA Redundancy](HighAvailability.md), +which covers OPC 10000-4 §6.6 server, client, subscription, session, and +network redundancy. For the base publisher/subscriber runtime, transports, +encodings, discovery, diagnostics, and Security Key Service (SKS), see +[Part 14 PubSub](PubSub.md). PubSub redundancy is implemented by the opt-in `Opc.Ua.Redundancy.PubSub` library and package `OPCFoundation.NetStandard.Opc.Ua.Redundancy.PubSub`. A deployment that uses only the standard `OPCFoundation.NetStandard.Opc.Ua.PubSub` package runs with the default process-local stores and `AlwaysActiveCoordinator`, so every configured PubSub component is active. +## Contents + +- [Overview](#overview-as-per-part-14-916) +- [How it maps onto the stack](#how-it-maps-onto-the-stack) +- [Sequence-number continuity](#sequence-number-continuity-hot-standby) +- [Shared security keys](#shared-security-keys-sks) +- [Consistency: eventual vs strong](#consistency-eventual-vs-strong) +- [Kubernetes](#kubernetes) +- [Security considerations](#security-considerations) +- [See also](#see-also) + ## Overview (as per Part 14 §9.1.6) OPC UA Part 14 §9.1.6 defines redundant PubSub sets as active/standby deployments. Redundant publishers share the same `PublisherId` and identical DataSetWriter configuration, including the same `WriterGroupId` and `DataSetWriterId`. Exactly one instance is `Active` and drives the transport; the rest are `Standby` and wait to take over. Redundant subscribers use the same pattern: one active subscriber dispatches received messages to the application, while standby subscribers keep enough state to assume ownership after failover. @@ -84,11 +101,25 @@ PubSub leases and leader election need compare-and-swap and therefore a strong k ## Kubernetes -Use the Kubernetes high-availability package when the redundant PubSub set runs as replicas in a cluster. `Opc.Ua.Redundancy.Kubernetes` provides Kubernetes Lease-based leader election for whole-instance active/standby deployments, so one pod in a `ReplicaSet` or `Deployment` drives the PubSub transports while the other pods remain standby. For the full server/client HA deployment pattern, RBAC, probes, time synchronization, peer discovery, and secret-management guidance, see [Kubernetes High Availability Deployment](Kubernetes.md). +Use the Kubernetes high-availability package when a redundant PubSub set runs +as replicas in a cluster. `Opc.Ua.Redundancy.Kubernetes` provides +Kubernetes-Lease leader election for whole-instance active/standby +deployments. One pod drives the PubSub transports; the others remain on +standby. + +For the full server/client deployment pattern, including role-based access +control (RBAC), probes, time synchronization, peer discovery, and secret +management, see [Kubernetes High Availability Deployment](Kubernetes.md). ## Security considerations -Treat the shared store as part of the PubSub trust boundary. Authenticate and encrypt the store channel, restrict network access to the redundant set, apply quotas and retention to PubSub key prefixes, and monitor lease, checkpoint, runtime-state, and SKS write failures as availability signals. +Treat the shared store as part of the PubSub trust boundary: + +- Authenticate and encrypt the store connection. +- Restrict network access to the redundant set. +- Apply quotas and retention to PubSub key prefixes. +- Monitor lease, checkpoint, runtime-state, and SKS write failures as + availability signals. Any shared secret or key material must be protected with `IRecordProtector` before it reaches a networked store. This is mandatory for SKS SecurityGroup records and for any future PubSub state that contains secrets. Never run production networked stores with `NullRecordProtector`; use a real protector such as an authenticated-encryption key-ring implementation and rotate its keys through the same operational controls used for OPC UA certificates and secrets. diff --git a/docs/README.md b/docs/README.md index 3da598b108..b075ee68a8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,98 +1,359 @@ # OPC UA .NET Standard stack documentation -Here is a list of available documentation for different topics: +These guides lead from a first working application to advanced and experimental +capabilities. Follow the numbered stages when you are learning. Choose only the +paths that match your application, or use the contents list to find a feature. -> **New contributor?** Start with the **[Developer Guide](DeveloperGuide.md)** — prerequisites, building, testing, coding standards, and how-to recipes (including how to add logging). +- **New to the SDK?** Start with [Getting started](GettingStarted.md), read + [OPC UA concepts](Concepts.md), then build + [your first information model](FirstModel.md). +- **Upgrading from 1.5.x?** Read [What's New in 2.0](WhatsNewIn2.0.md), then + follow the [Migration Guide](MigrationGuide.md). +- **Contributing to this repository?** See + [Contributing and maintaining the SDK](#contributing-and-maintaining-the-sdk). -> **Cutting, backporting, or promoting a release?** See the **[Release process](ReleaseProcess.md)** — the authoritative, step-by-step procedures for every release operation, including the branch/version model, backports, and recovering from a failed release. +## Contents -* [Sample applications](samples.md) - Platform-independent reference, PubSub, minimal API, companion-model, robotics, Vision, AI, ISA-95, and OpenUSD demos included in this repository. +- [How to use this index](#how-to-use-this-index) +- [1. Get started](#1-get-started) +- [2. Build your application](#2-build-your-application) + - [Client path](#client-path) + - [Server path](#server-path) + - [PubSub path](#pubsub-path) +- [3. Add data types and application services](#3-add-data-types-and-application-services) + - [Data types and models](#data-types-and-models) + - [Application services](#application-services) +- [4. Use companion models](#4-use-companion-models) +- [5. Prepare for production](#5-prepare-for-production) +- [6. Advanced integration, reliability, and scale](#6-advanced-integration-reliability-and-scale) + - [Transports and integration](#transports-and-integration) + - [Session recovery and performance](#session-recovery-and-performance) + - [Security infrastructure and fleet management](#security-infrastructure-and-fleet-management) + - [High availability and deployment](#high-availability-and-deployment) + - [Native AOT publishing](#native-aot-publishing) +- [7. Draft and experimental capabilities](#7-draft-and-experimental-capabilities) +- [Reference and upgrade](#reference-and-upgrade) +- [Contributing and maintaining the SDK](#contributing-and-maintaining-the-sdk) -## UA Core stack related +## How to use this index -* [OPC UA Profiles and Facets](Profiles.md) - Overview of supported OPC UA profiles, facets, security policies, and transport protocols. -* [Transport Profiles](Transports.md) - Developer guide for the wire transports: `opc.tcp` / HTTPS (binary + JSON + REST) / WSS (binary + JSON), including server hosting and client connect examples. -* [REST Binding (OpenAPI Mapping)](WebApi.md) - OPC UA Part 6 §G.3 OpenAPI mapping: ASP.NET Core MVC controllers for every spec service, Compact / Verbose encoding negotiation, four pluggable auth modes (Anonymous / Bearer JWT / HTTP Basic / Mutual TLS), and the symmetric `IWebApiClient`. -* [What's New in 2.0](WhatsNewIn2.0.md) - Developer-facing tour of the 1.5.378 → 2.0 changes, grouped by theme and layer, with links to deeper feature docs. -* [Migration Guide](MigrationGuide.md) - How to migrate from a previous version. -* [Sessions, Reconnection, and Subscription Engines](Sessions.md) - Architectural overview of `Session`, `ManagedSession`, `SessionReconnectHandler`, and the classic / V2 subscription engines, including guidance on which to use. -* About [.NET platform support, NuGet packages and versioning](DeveloperGuide.md#packages-platform-support-and-versioning). -* About [continuous integration](DeveloperGuide.md#continuous-integration) — which pipeline runs what, how to start a validation build on a pull request with `/azp run`, and the coverage gates a change has to satisfy. -* How X.509 [Certificates](Certificates.md) are used in the certificate stores. -* [CertificateManager](CertificateManager.md) - Centralized certificate lifecycle management, server-side push certificate rotation, and the OPC UA Part 12 PushManagement transaction model (`ApplyChanges`/`CancelChanges`, staged TrustList/Certificate updates, `DeleteCertificate`, pending-key persistence). -* [Crypto provider](CryptoProvider.md) - Replacing the stack's cryptography with another library, a remote service or hardware (TPM, HSM, PKCS#11, cloud key service), keeping private keys inside the device, selecting providers per purpose and security policy, and auditing the use of uncertified cryptography. -* Using the [Reverse Connect](ReverseConnect.md) for the UA-TCP and WSS transports. -* Support for the [TransferSubscriptions](TransferSubscription.md) service set. -* [Diagnostics](Diagnostics.md) — logging, telemetry, server audit events, server diagnostics nodes, and packet capture. -* [Performance Benchmarks](Benchmarks.md) — BenchmarkDotNet methodology, the 2.0 (`master`) vs 1.5.378 (`master378`) comparison, root-cause analysis of the encoder/decoder/session regressions and their real-world impact, the subscription-notification (pooled encodeable) micro-benchmarks, server session scalability (the 500-session capability, sizing and bottlenecks), and planned future work. -* [Server Session Scalability](ServerScalability.md) — why a single node tops out at ~2000 concurrent sessions, the establishment vs steady-state boundaries (socket backlog, the `BadTcpInternalError` retry-storm amplifier, the O(N²) diagnostics rescan, CreateSession crypto-under-lock, the RSA CPU wall, and the held-Publish worker-accounting coupling) with code references, and a prioritized admission-control / rate-limiting roadmap for moving beyond it. -* [Server resource isolation](ResourceIsolation.md) - Configurable shared/fair/balanced/trusted admission, protected resource floors, bounded caller accounting, and fair decoded-request scheduling. -* [Rate Limiting and Admission Control](RateLimiting.md) — the server's deterministic, configurable connection- and session-establishment limiters (on by default, `System.Threading.RateLimiting`-based, DI-pluggable), shared incomplete-message capacity and sessionless headroom, the `BadServerTooBusy` signalling, the diagnostics-independent server retry-after carriers (`ResponseHeader.additionalHeader`, HTTP `Retry-After`, UA-TCP ERR, and load-based `Server.ServiceLevel`), and the client's server-signal-aware adaptive reconnect backoff (`IReconnectPolicy.TryGetNextDelay`). -* Support for [WellKnownRoles & RoleBasedUserManagement](RoleBasedUserManagement.md). -* Pluggable [Identity Providers](IdentityProviders.md) — interfaces (`IClientIdentityProvider`, `IUserTokenAuthenticator`, `IAccessTokenProvider`, `ITokenIssuer`, `IIdentityClaims`) plus the OPC 10000-6 §6.5.2.2 `IssuerEndpointUrl` JSON parser for OAuth2 / OIDC / Entra / JWT flows. -* Support for [ECC Certificates](EccProfiles.md). -* Working with [ComplexTypes](ComplexTypes.md) - Custom structures and enumerations. -* Client-based [NodeSet Export](NodeSetExport.md) - Export server address space to NodeSet2 XML. -* Source generated [DataTypes] - How to annotate POCO classes and let the source generator generate the `IEncodeable` implementation. -* Runtime [Schema Generation](SchemaGeneration.md) - Produce XSD, OPC Binary (BSD) and JSON Schema (Part 6 Annex C, compact + verbose) for generated encodeable types and dynamically added complex types via the injectable `ISchemaProvider`; schemas are built as object models in code (trimmable, NativeAOT compatible). -* [NodeManagers](NodeManagers.md) - Overview of the server node-manager architecture, built-in managers (master, core, diagnostics/configuration), CoreNodeManager vs CustomNodeManager2 guidance, registration and runtime lifecycle rules, namespace metadata and historical-access reconciliation, and source-generated `AsyncCustomNodeManager` authoring with the fluent `INodeManagerBuilder` API. Includes NativeAOT single-file server guidance and samples such as [MinimalBoilerServer](../samples/MinimalApi/MinimalBoilerServer) and [PumpDeviceIntegrationServer](../samples/DI/PumpDeviceIntegrationServer). -* Runtime [NodeSets](RuntimeNodeSets.md) - Load one or more NodeSet2 XML documents into the server address space at startup without source generation. Covers file and stream sources, dependency ordering, parent-child browse-path resolution, fluent `Configure` callbacks, default namespace inference, and the default complex-type loading path. Use when the XML content changes independently of the server binary or for rapid prototyping. -* [WoT / NodeSet conversion](WoTNodeSetConversion.md) - `WotNodeSetConverter` readable mapping, preservation projections, and the WoT-to-NodeSet default/failure table. -* [WoT Connectivity](WoTConnectivity.md) - OPC 10100-1 asset connectivity, the WoT Connectivity 1.1 registry/client, dependency-closure materialization, and runtime NodeSet projection. -* [WoT protocol bindings](WotBindings.md) - The bindings that ship today (planner/executor architecture, bundled and separate packages, operation coverage, target mapping, lazy channels, generation lifetime) and the contributor guide for adding your own, with a complete memory binding, registration, diagnostics, tests, packaging, TFM, trimming, and NativeAOT guidance. -* [WoT aggregation sample](../samples/WotCon/README.md) - Two flat OPC UA sources aggregated into a runtime-loaded DI/Machinery/Pumps Pump model, including commands, Refresh, monitoring, replacement, troubleshooting, and NativeAOT publishing. -* [Device Integration (DI) developer guide](DeviceIntegration.md) - End-to-end documentation for the `Opc.Ua.Di*` library trio: fluent `IDeviceBuilder`, device sub-type extensions (`AddSoftware`, `AddBlock`, `AddConfigurableObject`, `AddLifetimeIndication`, `WithSupportInfo`), hosting integration (`AddOpcUaDi` / `ConfigureDevicesFor`), lock service, software-update package store, and client helpers (`DiLockClient`, `DiTopologyClient`, `SoftwareUpdateClient`). Includes a section enumerating supported OPC 10000-100 features against the spec. -* [OpenUSD](OpenUsd.md) — bridge an OPC UA address space to an OpenUSD stage, in two parts. **Part 1 — bindings**: the generic domain-agnostic `OpenUsdConnector` (discovers `Server/OpenUSD/Representations`, subscribes, composes, verifies stage/asset digests, replays history), the `Variant`-based `IUsdSink` with `UsdFileSink` / `MockUsdSink`, the fluent/DI `AddOpenUsdConnector` extensions + `OpenUsdConnectorFactory` / `OpenUsdConnectorOptions`, server-side `UsdAssetDelivery`, and the optional `--view` viewport. **Part 2 — scene materialization**: materializes a composed USD stage *inside* the address space so the prim tree is the node hierarchy — the `Opc.Ua.OpenUsd.Scene` companion and scene-document APIs, `Opc.Ua.OpenUsd.Scene.Conversion` `.usda` reader/writer and §6.2 `UsdValueTypeMap` (USD roles as DataTypes subtyping their built-in), and `Opc.Ua.OpenUsd.Server.Scene` materialization/export APIs with unknown-type fallbacks, Mode-A live attributes, portable Cesium georeference dual-authoring, discovery and Part 1 binding-target resolution. Both parts ship in the `Opc.Ua.OpenUsd` and `Opc.Ua.OpenUsd.Server` packages, and both companion models are currently drafts. -* [Robotics developer guide](Robotics.md) — the `Opc.Ua.Robotics` / `Opc.Ua.Robotics.Server` / `Opc.Ua.Robotics.Client` trio for OPC 40010 Robotics 1.02 over OPC 40001-1 IA and OPC 10000-100 DI: source-generated models, `AddRobotics` / `AddRoboticsModel` / `ConfigureRobotics(For)` hosting, `IRoboticsModelProvider` / `IRoboticsConfigurator` / `IRoboticsBuildContext`, validated fluent topology builders (`AddMotionDeviceSystemAsync` down to axes, power trains, motors, gears, drives, safety states, and task controls), semantic references, `ArrayOf` snapshot contracts, and `RoboticsClient` discovery. It also covers [Robot Intent](Robotics.md#robot-intent), the draft task-level motion verbs OPC 40010 leaves undefined, including the MCP surface for LLM agents. -* [Vision developer guide](Vision.md) — the `Opc.Ua.Vision` / `Opc.Ua.Vision.Server` / `Opc.Ua.Vision.Client` / `Opc.Ua.Vision.OpenUsd` package family for the draft *OPC UA — Vision* companion specification: source-generated Vision model, `AddVision` / `ConfigureVision` hosting with `IVisionMediaProvider` / `IVisionInferenceProvider` / `IVisionFeedbackSink`, the two perception paths behind one contract (`OnServer` deterministic detector vs `EdgeOffServer` agent submissions), fluent topology builders for frames, sensors, calibrations, media endpoints and inference pipelines, `VisionClient` discovery + `VisionFrameGraph` §5.12 pose composition + `VisionResultReader` streaming detections + `VisionFeedbackClient` for off-server VLM agents, the §6.4 media-gating states, the `NoRenderingBackend` degrade path, facet derivation, and the composed [`vision` MCP profile](McpServer.md) with the [BinPickingCell / BinPickingClient](../samples/Robotics/BinPickingCell) example. -* [AI Model Management developer guide](AI.md) — the `Opc.Ua.AI` / `Opc.Ua.AI.Inference` / `Opc.Ua.AI.Server` / `Opc.Ua.AI.Client` package family for the draft *OPC UA — AI Model Management and Inference* companion specification over xRegistry: source-generated catalogues, datasets, deployments, inference endpoints and learning jobs, the `IInferenceBackend` contract with `Microsoft.Extensions.AI` `IChatClient` and OpenAI-compatible REST backends, `AINodeManagerFactory` hosting via `AddNodeManager`, `Invoke` routing, standard file-transfer artefact streaming, credential resolvers that keep secret material out of the address space, and the [ModelManagementServer / ModelManagementClient](../samples/AI/README.md) example. -* [Relative Spatial Location and Global Positioning](Positioning.md) — source-generated OPC 10000-210 RSL and OPC 10000-211 GPOS models, standalone/composed server hosting, provider contracts, high-level clients, frame-chain resolution, WGS84/ENU conversion, and ground-control-point fitting. -* [Generators (generating sets)](../samples/OpenUsd/GeneratorServer/Generators.md) — the draft Generators companion specification realised end to end by [GeneratorServer](../samples/OpenUsd/GeneratorServer): a datasheet-driven simulation in which load fraction is the only independent variable, DI + Machinery integration, and one independent OpenUSD twin per configured set. Includes [SiteCompositionServer](../samples/OpenUsd/SiteCompositionServer), a supervisory server that owns no devices and composes the pump and generator servers into a single scene through cross-server components. -* [ISA-95 developer guide](ISA95.md) - End-to-end documentation for the `Opc.Ua.ISA95*` library trio: the OPC-10030 Common Model and OPC-10031-4 Job Control V1/V2, the two transparently-documented normative NodeSet repairs, `AddIsa95Server`/`AddIsa95Client` hosting, the typed common-model builder, the shared Job Control state engine and its `Uncertain`/Annex-B `ReturnStatus` result model, the provider-backed `GeoSpatialLocationType` seam and planned Part 210/211 RSL/GPOS integration, and a conformance matrix distinguishing static NodeSet structure from runtime-tested behavior. -* [Alias Names](AliasNames.md) - Full server + client support for the OPC UA Part 17 alias-name model (`AliasNameType`, `AliasNameCategoryType`, `FindAlias`, `FindAliasVerbose`, `AddAliasesToCategory`, `DeleteAliasesFromCategory`, `LastChange`). -* [Alarms and Conditions](AlarmsAndConditions.md) - Full server + client support for OPC UA Part 9. Server-side state types for latched/silenced/out-of-service alarms, alarm groups and suppression engine, alarm rate metrics. Client-side `AlarmClient`, typed alarm event records, fluent `AlarmEventFilterBuilder`, `IAsyncEnumerable` alarm streaming via `AlarmStreamExtensions`. -* [Historical Access (Part 11)](HistoricalAccess.md) - Server provider model (`IHistorianProvider` family) and `InMemoryHistorianProvider`, plus the client `HistoryClient` (`session.Historian()`) for raw/modified/at-time/processed/event reads, modification audit metadata, annotations, and data/event/structured updates. -* [Aggregates (Part 13)](Aggregates.md) - All 37 standard Part 13 v1.05.07 aggregate functions over historical data: server `AggregateManager` / calculators, native push-down vs framework fallback, `AnnotationCount` via the annotation provider, `AggregateConfiguration` defaults, and the client `ReadProcessedAsync` helper. -* [Subscriptions and Monitored Items Service Set](Subscriptions.md) - V2 subscription engine API. Covers `ISubscriptionManager` for long-lived callback-based subscriptions, the declarative+imperative `SetTriggering` API with N:M support and automatic replay on recreate/reconnect, and `IStreamingSubscription` (`IAsyncEnumerable`-based) for state-machine waits and short-lived monitoring (`ManagedSession.DefaultStreaming`, `TakeUntilAsync` / `WithTimeoutAsync` helpers). -* [Unbounded Monitored Items](Subscriptions.md#unbounded-monitored-items) - V2 logical-subscription wrapper that transparently splits monitored items across multiple server-side partitions when the per-subscription cap is exceeded (`IPartitionedSubscription`, `MonitoredItemOptions.Affinity`, reactive `Bad_TooManyMonitoredItems` fallback, secondary-partition idle-delete). -* [State Machines](StateMachines.md) - Generic, extensible Part 16 state-machine API. Client side: streaming + read helpers on the source-generated `*TypeClient` proxies (`GetCurrentFiniteStateAsync`, `ObserveFiniteTransitionsAsync`, `WaitForStateAsync`). Server side: unified fluent `StateMachineBuilder` with two complementary modes — *definition* (`Create(...)` + `AddState` / `AddTransition` / `OnCause` for ad-hoc machines via `FluentFiniteStateMachineState`) and *lifecycle* (`For(...)` / `INodeBuilder.AsStateMachine()` + `OnEnterState` / `WithCause` / `WithTimedTransition` to attach behavior to stack-shipped or generator-emitted FSMs). Vendor state machines inherit both ends of the API automatically. -* [Model Change Tracking](ModelChangeTracking.md) - Client-side address-space change tracking with per-node `INodeCache` invalidation; server-side `ModelChangeAggregator` and auto-emitted `GeneralModelChangeEvent` from `CustomNodeManager.CreateNode/DeleteNode`. -* [NodeManagement Service Set](NodeManagement.md) - Server-side AddNodes / DeleteNodes / AddReferences / DeleteReferences, including the `INodeManagementAsyncNodeManager` opt-in pattern and per-NodeManager `AllowNodeManagement` gate. -* [High Availability and OPC UA Redundancy](HighAvailability.md) - OPC 10000-4 §6.6 mapping for server, client, and network redundancy; `RedundancySupport`, `ServiceLevel`, manual failover, transparent/non-transparent `ManagedSession` modes, HotAndMirrored/Transparent state mirroring, distributed address-space/session/subscription stores with CRDT (eventual) or Raft (strong) consistency, shared certificate/trust-list stores, snapshot+delta hydration, and the optional GetEndpoints load-direction seam. - * [Replica-consistent NodeIds](ReplicaNodeIdentity.md) - Required fixed shared namespace layout, guarded factory assignment, protected store/peer contracts, writer-created identities, and unchanged client NodeIds across failover. - * [Kubernetes High Availability Deployment](Kubernetes.md) - Consolidated Kubernetes guide for the `Opc.Ua.Redundancy.Kubernetes` package: Lease leader election, EndpointSlice peer discovery, ServiceLevel-driven readiness, StatefulSet/Deployment and Service manifests, RBAC, probes, time sync, secrets, and GDS/NTRS registration. - * [Redundant Sample Integration Tests](RedundancySampleTests.md) - Process-level integration tests that launch the `RedundantServer` / `RedundantClient` / `RedundantPubSub` sample apps and assert on their failover, reconnect, and data-loss-visibility behavior; short-haul variants run on every PR, long-haul soak variants run via dedicated manual/scheduled GitHub Actions and Azure DevOps jobs (`SAMPLE_HA_DURATION_MINUTES`). -* [Dependency Injection](DependencyInjection.md) - The unified `services.AddOpcUa()` / `IOpcUaBuilder` surface for hosting OPC UA components in `Microsoft.Extensions.DependencyInjection` / the .NET Generic Host (servers as `IHostedService`, options via `Action` or `IConfiguration`, AOT-friendly). -* [AuthorizationService](AuthorizationService.md) - Modern Part 12 `StartRequestToken` / `FinishRequestToken`, `ITokenIssuer`, and GDS token issuance. -* [Fuzz testing](../fuzzing/Fuzzing.md) - SharpFuzz + afl-fuzz + libFuzzer integration. Three areas: `Encoders` (Binary/JSON/XML decoders, built-in type readers, parser entry points), `Certificates` (`X509CRL`, X509 extension parsers, `PEMReader`, `Pkcs10CertificationRequest`, ASN.1 helpers), and `Network` (UA-SC framing via `Opc.Ua.Core.Diagnostics` + internal `TcpMessageParsers` seam on `Opc.Ua.Core`). The [`fuzz-tester`](../.github/agents/fuzz-tester.agent.md) custom agent drives the whole toolchain autonomously: it detects OS-available engines, runs them in parallel, fixes novel findings per repo guidelines, adds the failing input as a regression asset, and pushes one commit per fix until the user says stop. -* [KeyCredentialService](KeyCredentialService.md) - Pull, Push, and experimental bridge guidance for Part 12 KeyCredential flows. -* [xRegistry (abstract registry base model)](XRegistry.md) - Generic registry with structural `Xid` identity and a separate `IResourceContentIdProvider` Opaque-NodeId document fast path, true dirty-`Close` registration, federated resource proxies, native events, and resource-exhaustion bounds. Shipped as `Opc.Ua.XRegistry`, `Opc.Ua.XRegistry.Client`, and `Opc.Ua.XRegistry.Server`. -* [PubSub (Part 14)](PubSub.md) - Publisher/subscriber support library: architecture, fluent builder, transports (UDP / MQTT 3.1.1 + 5.0 / Kafka / Ethernet Layer 2), encodings (UADP / JSON), security, and server-side address space. - * [PubSub High Availability](PubSubHighAvailability.md) - Distributed Part 14 §9.1.6 active/standby publishers and subscribers with Cold/Warm/Hot modes, leader election or fenced leases, shared runtime checkpoints, protected SKS key storage, consistency guidance, and Kubernetes deployment notes. - * [Migration sub-doc](migrate/2.0.x/pubsub.md) - 1.5.378 → 2.0 breaking API, transport, JSON, and field-encoding changes, plus the compatibility matrix. - * [Ethernet transport](PubSub.md#transports) - Layer 2 PubSub (`opc.eth://`, EtherType `0xB62C`, 802.1Q VLAN) with native AF_PACKET / BPF, SharpPcap, and in-memory backends. - * [Kafka transport](PubSub.md#apache-kafka) - Apache Kafka broker transport (`kafka://`, `kafkas://`) for UADP and JSON PubSub profiles with SASL/TLS and NativeAOT support on `net10.0`. - * [PubSub Transcoding](PubSub.md#transcoding) - In-process subscriber-to-publisher transcoding with UADP/JSON cross-encoding, field/value/metadata transforms, identifier remap, receive hooks, egress, and managed UADP re-securing. - * [External server adapter](PubSub.md#binding-pubsub-to-an-external-opc-ua-server-client-session-adapters) - Bind PubSub publishers, subscribers, and Action responders to an external OPC UA server through `ManagedSession`. - * [Dependency Injection extensions](DependencyInjection.md) - `AddPubSub`, `AddPubSubPublisher`, `AddPubSubSubscriber`, `AddPubSubSecurityKeyServiceClient/Server`, `AddPubSubAddressSpace`. - * [Profiles](Profiles.md#pubsub-transports) - Datagram-v2, SKS pull / push, AES-128/256-CTR security facets. - * [PubSub Diagnostics](Diagnostics.md#5-pubsub-packet-capture-and-dissection) - packet capture, dissection and replay of UDP / MQTT PubSub traffic, including decryption of encrypted UADP messages. +Each stage builds on the earlier ones, but you do not need to read every guide. +After stage 1, choose the client, server, or PubSub path. Stages 3 to 7 are +optional; use the guides your application needs. A stage or group names the +knowledge it assumes. -## Reference application related +Labels describe the maturity of a package or model: -* [Reference Client](../samples/Reference/ConsoleReferenceClient/README.md) documentation for configuration of the console reference client using parameters. -* [Reference Server](../samples/README.md) documentation for running against CTT. -* [ConsoleReferencePubSubClient](../samples/PubSub/ConsoleReferencePubSubClient/README.md) documentation for the PubSub reference sample (publisher / subscriber / external-server adapter modes). -* [Provisioning Mode](ProvisioningMode.md) for secure certificate provisioning and initial server configuration. -* Using the [Container support](ContainerReferenceServer.md) of the Reference Server in Visual Studio 2026 and for local testing. +- **Preview package**: the package family keeps a `-preview` version, even when + the core packages are stable. See [Versioning](DeveloperGuide.md#versioning). +- **Draft model**: the guide implements a working-group draft specification. + Its namespace URIs and NodeIds are provisional. +- **Experimental**: the guide marks the feature as experimental. -Starting with version 1.5.375.XX the Windows Forms reference client & reference server were moved to the [OPC UA .NET Standard Samples](https://github.com/OPCFoundation/UA-.NETStandard-Samples) repository. +These labels are independent of difficulty. An advanced guide can describe a +stable feature, and a released specification can ship in a preview package. -## Global Discovery Server (GDS) +## 1. Get started -* [GDS Developer Guide](GDS.md) — Application registration, certificate management (pull & push models), roles and authorization, OPC 10000-21 registrar ticket administration, provider implementation, the runnable [`samples/Gds`](../samples/Gds/README.md) onboarding demo, and a per-requirement [OPC UA Part 12 conformance matrix](GDS.md#conformance-matrix). -* [KeyCredentialService](KeyCredentialService.md) — Credential issuance for non-OPC UA services (MQTT, REST), IKeyCredentialRequestStore provider guide, ISecretStore integration. -* [AuthorizationService](AuthorizationService.md) — OAuth2-style access token issuance, IAccessTokenProvider implementation guide. -* [Role-Based Security](RoleBasedUserManagement.md) — Part 18 roles and claim-based identity-mapping rules. -* [Identity Providers](IdentityProviders.md) — server and client identity-provider architecture. -* [Dependency Injection](DependencyInjection.md) — dependency injection hosting and identity registration extensions. +Run a client and server, then learn the terms that the other guides use. + +- [Getting started](GettingStarted.md) — run the minimal samples, or build a + client and server from NuGet packages. +- [OPC UA concepts](Concepts.md) — applications, endpoints, sessions, the + address space, values, subscriptions, and PubSub. +- [Your first information model](FirstModel.md) — add a model to the Getting + started server, then read, write, and call it from the client. +- [Sample catalogue](Samples.md) — minimal, reference, PubSub, and + companion-model applications. +- [Packages and supported platforms](DeveloperGuide.md#packages-platform-support-and-versioning) + — choose packages and target frameworks. + + + +## 2. Build your application + +Choose the path for your application. The paths are independent: for example, +a PubSub application needs neither a client nor a server. + +### Client path + +Assumes [Getting started](GettingStarted.md). + +1. [Dependency injection: client feature](DependencyInjection.md#client-feature) + — configure a client in the .NET Generic Host. +2. [Managed sessions](Sessions.md#3-managedsession--the-connection-state-machine-facade) + — connect, recover from connection loss, and close. To compare the managed + and classic session APIs, see [choosing a session API](Sessions.md#7-putting-it-all-together). +3. [Read, write, and call methods](FirstModel.md#3-use-the-model-from-the-client) + — resolve namespace URIs and browse paths, check the status of each + operation, and call methods through a client generated from the model. +4. [Subscriptions](Subscriptions.md) — callback subscriptions for long-lived + monitoring and streams for short observations. + +### Server path + +Assumes [Getting started](GettingStarted.md). + +1. [Dependency injection: server feature](DependencyInjection.md#server-feature) + — host a server and configure its endpoints, security, and node managers. +2. [Node managers: start with a model](NodeManagers.md#start-with-a-model) — + generate a node manager from your model and attach callbacks. + [Your first information model](FirstModel.md) is a complete example. +3. [Runtime NodeSets](RuntimeNodeSets.md#quick-start-examples) — load NodeSet2 + models without generated code when models change independently of your + application. +4. [Asynchronous server support](AsyncServerSupport.md) — implement service and + node-manager callbacks asynchronously. + +### PubSub path + +A PubSub application needs neither a client nor a server, so the Getting +started exercise and the other two paths are optional here. + +1. [Concepts: client/server and PubSub](Concepts.md#clientserver-and-pubsub) — + publishers, subscribers, and datasets. +2. [Run a first publisher and subscriber](PubSub.md#run-a-first-publisher-and-subscriber) + — exchange signed and encrypted messages on one computer with the + [reference PubSub sample](../samples/PubSub/ConsoleReferencePubSubClient/README.md). +3. [PubSub guide](PubSub.md) — the fluent builder, hosting, configuration, + encodings, and security. +4. [Transport selection](PubSub.md#transports) — UDP, Ethernet, MQTT, and Kafka. + +## 3. Add data types and application services + +Assumes the client or server path. Choose the capabilities your application +needs. + +### Data types and models + +- [Source-generated data types](SourceGeneratedDataTypes.md) — generate + encodeable implementations for your own structures. +- [Complex types](ComplexTypes.md) — decode server-defined structures and + enumerations at run time. +- [Model dependencies](ModelDependencies.md) — compose models that depend on + other models. +- [NodeId assignment](NodeIdAssignment.md) — control the identifiers of nodes + created at run time. +- [Schema generation](SchemaGeneration.md) — produce XSD, BSD, and JSON schemas + for data types. + +### Application services + +- [State machines](StateMachines.md) — define, drive, and observe state + transitions. +- [Alarms and conditions](AlarmsAndConditions.md) — raise, acknowledge, and + stream alarms. Alarm conditions use state machines, for example for shelving. +- [Historical access](HistoricalAccess.md) — store and read value and event + history. +- [Aggregates](Aggregates.md) — calculate processed history; read historical + access first. +- [File system client](FileSystemClient.md) — transfer files through the + standard file objects. +- [Alias names](AliasNames.md) — find nodes through named aliases. +- [Node management](NodeManagement.md) — let clients add and delete nodes and + references. +- [Model change tracking](ModelChangeTracking.md) — keep client caches current + when the address space changes. +- [NodeSet export](NodeSetExport.md) — export a server's address space to + NodeSet2 XML. + +## 4. Use companion models + +Companion specifications define models for particular industries and device +types; [Concepts](Concepts.md#information-models-and-companion-specifications) +explains how they relate to the base model. These libraries assume the server +path and, for typed client access, the client path. The packages load the +models that each model depends on. + +- [Device Integration](DeviceIntegration.md) (OPC 10000-100; preview package) — + devices, topology, and locking. +- [Software update](SoftwareUpdate.md) (OPC 10000-100; preview package) — + software package storage, installation, and confirmation. Read Device + Integration first. +- [ISA-95](ISA95.md) (OPC 10030 and OPC 10031-4; preview package) — the common + model and job control. +- [Positioning](Positioning.md) (OPC 10000-210 and OPC 10000-211; preview + package) — relative frames and geographic positions. +- [Robotics](Robotics.md) (OPC 40010-1; preview package) — motion-device + systems, based on the Industrial Automation and Device Integration models. + The guide's draft Robot Intent model is listed in [stage 7](#7-draft-and-experimental-capabilities). +- [WoT connectivity](WoTConnectivity.md) (OPC 10100-1; preview package) — + connect Web of Things assets. Its registry extension is listed in stage 7. + +For more examples, including the Pumps and Machinery models, see the +[companion-model samples](Samples.md#companion-model-samples). + +## 5. Prepare for production + +Builds on the certificate trust you used in [Getting started](GettingStarted.md). +Before you deploy a client or server, work through the +[production readiness checklist](ProductionChecklist.md). It links to these +guides for the details: + +1. [Certificates](Certificates.md) — certificate stores, trust, and chain + validation. +2. [Identity providers](IdentityProviders.md) — authenticate users on clients + and servers. +3. [Role-based user management](RoleBasedUserManagement.md) — map identities to + roles and permissions. +4. [Certificate manager](CertificateManager.md) — certificate lifecycle, + rotation, and remote updates. +5. [Diagnostics](Diagnostics.md) — logging, metrics, tracing, auditing, and + packet capture. +6. [Rate limiting](RateLimiting.md) — admission limits and busy signals during + connection storms. +7. [Server resource isolation](ResourceIsolation.md) — per-caller limits and + reserved capacity for startup and recovery. +8. [Profiles and facets](Profiles.md) — implemented profiles and platform + boundaries. +9. [Container reference server](ContainerReferenceServer.md) — run the + reference server in a container. + +## 6. Advanced integration, reliability, and scale + +Each group names the stages it assumes. + +### Transports and integration + +Assumes the client or server path. + +- [Transports](Transports.md) — UA-TCP, HTTPS, and WebSocket transports. +- [REST binding](WebApi.md) — OpenAPI mapping, encoding negotiation, and HTTP + authentication. +- [Reverse Connect](ReverseConnect.md) — server-initiated connections. +- [PubSub external-server adapter](PubSub.md#binding-pubsub-to-an-external-opc-ua-server-client-session-adapters) + — bind PubSub to an OPC UA server through a managed client session. Assumes + the client and PubSub paths. +- [PubSub transcoding](PubSub.md#transcoding) — convert encodings, values, and + metadata between PubSub messages. +- [MCP server](McpServer.md) — let AI agents browse, read, write, and diagnose + OPC UA servers. +- [WoT / NodeSet conversion](WoTNodeSetConversion.md) and + [WoT protocol bindings](WotBindings.md) — convert Thing Descriptions and add + protocol bindings. Read WoT connectivity first. + +### Session recovery and performance + +The subscription guides assume the client path; the sizing guides assume +[Diagnostics](Diagnostics.md). + +- [TransferSubscriptions](TransferSubscription.md) — move server subscriptions + to a new session. +- [Durable subscriptions](DurableSubscription.md) — keep subscriptions and + queued notifications while clients are disconnected. +- [Server scalability](ServerScalability.md) — sizing limits and bottlenecks. +- [Benchmarks](Benchmarks.md) — measured performance and methodology. + + + +### Security infrastructure and fleet management + +Assumes [stage 5](#5-prepare-for-production). + +- [ECC profiles](EccProfiles.md) — elliptic-curve security policies and their + platform requirements. +- [Crypto providers](CryptoProvider.md) — replace the platform cryptography and + keep private keys in hardware. +- [Provisioning mode](ProvisioningMode.md) — configure a new server securely + before exposing application data. +- [Global Discovery Server](GDS.md) — application registration and central + certificate management. +- [GDS sample](../samples/Gds/README.md) — a runnable onboarding workflow. +- [AuthorizationService](AuthorizationService.md) — issue access tokens. +- [KeyCredentialService](KeyCredentialService.md) — manage credentials for + services that are not OPC UA. Its issued-token bridge is listed in stage 7. + +### High availability and deployment + +Assumes a working single server and [stage 5](#5-prepare-for-production). + +- [High availability](HighAvailability.md) (preview package) — redundancy, + failover, and distributed state. +- [Replica-consistent NodeIds](ReplicaNodeIdentity.md) (preview package) — keep + node identifiers identical across replicas; required before replicating an + address space. +- [Kubernetes](Kubernetes.md) (preview package) — cluster deployment, + discovery, readiness, and secrets. Read high availability first. +- [PubSub high availability](PubSubHighAvailability.md) (preview package) — + redundant publishers and subscribers. Assumes the PubSub path. + +### Native AOT publishing + +Publish an application as a native executable without a JIT compiler; start +with [Native AOT: publish an application](NativeAoT.md#publish-an-application). +Feature-specific guidance: + +- [Servers with generated node managers](NodeManagers.md#nativeaot-publishing) +- [Applications hosted with dependency injection](DependencyInjection.md#native-aot) +- [PubSub applications](PubSub.md#native-aot) + +## 7. Draft and experimental capabilities + +These guides implement working-group drafts or experimental extensions. +Namespace URIs, NodeIds, and APIs can change before the specifications are +published, so evaluate them before you depend on them in production. Each +guide describes its current limitations. + +- [xRegistry](XRegistry.md) (draft model; preview package) — the generic + registry base model, resource storage, and federation. +- [AI model management](AI.md) (draft model; preview package) — model + catalogues, deployments, inference, and learning jobs. Builds on xRegistry. +- [Vision](Vision.md) (draft model; preview package) — perception, media, + feedback, and client workflows. +- [Robot Intent](Robotics.md#robot-intent) (draft model; preview package) — + task-level motion commands. Does not require the Robotics model. +- [OpenUSD](OpenUsd.md) (draft models; preview package) — bind values to OpenUSD + scenes and materialize scenes in the address space. +- [WoT Connectivity 1.1 registry](WoTConnectivity.md#11-wot-connectivity-11-registry-and-materialization-preview) + (draft model; preview package) — registry and materialization. Read WoT + connectivity first. +- [KeyCredential issued-token bridge](KeyCredentialService.md#experimental-keycredential-issued-token-bridge) + (experimental). + +These samples combine several draft or preview capabilities. Read the guides +for their models first: + +- [Bin picking cell](../samples/Robotics/BinPickingCell/README.md) — Robot + Intent, Vision, and an OpenUSD scene. +- [Visual inspection cell](../samples/Vision/VisualInspectionCell/README.md) — + Vision, AI model management, and ISA-95 job control. +- [WoT aggregation sample](../samples/WotCon/README.md) — load Device + Integration, Machinery, and Pumps models from WoT documents through the WoT + Connectivity 1.1 registry, and bind them to two OPC UA source servers. +- [Generator sample model](../samples/OpenUsd/GeneratorServer/Generators.md) + (draft model) — generating-set simulation and scene composition. + + + +## Reference and upgrade + +- [What's New in 2.0](WhatsNewIn2.0.md) — a summary of new capabilities. +- [Migration Guide](MigrationGuide.md) — upgrade from 1.5.378, with + [topic-specific 2.0 guides](migrate/2.0.x/README.md) and + [PubSub migration](migrate/2.0.x/pubsub.md). +- [Reference client](../samples/Reference/ConsoleReferenceClient/README.md) — + command-line configuration and transport profiles. +- [Reference server](../samples/README.md) — hosting and compliance-test setup. +- [Reference PubSub client](../samples/PubSub/ConsoleReferencePubSubClient/README.md) + — publisher, subscriber, and external-adapter modes. + +The Windows Forms reference applications are maintained in the +[Samples repository](https://github.com/OPCFoundation/UA-.NETStandard-Samples). + +## Contributing and maintaining the SDK + +These documents describe building and maintaining this repository, not using +the SDK in an application: + +- [Developer Guide](DeveloperGuide.md) — prerequisites, build, tests, and coding + conventions. +- [Continuous integration](DeveloperGuide.md#continuous-integration) — + pipelines and coverage gates. +- [Native AOT test harness](NativeAoT.md#test-harness-overview) — build, run, + and extend the Native AOT compatibility tests. +- [Release process](ReleaseProcess.md) — release branches, versioning, + backports, and recovery. +- [Redundancy sample tests](RedundancySampleTests.md) — process-level failover + and soak validation. +- [Fuzz testing](../fuzzing/Fuzzing.md) — encoder, certificate, and network + campaigns. diff --git a/docs/RateLimiting.md b/docs/RateLimiting.md index 0b4f799ddf..c8aad676c1 100644 --- a/docs/RateLimiting.md +++ b/docs/RateLimiting.md @@ -13,6 +13,18 @@ deployment: even legitimate clients can be refused during a sufficiently large burst. Disabling rate limiting does not disable [resource isolation](ResourceIsolation.md) or the incomplete-message budget. +## Contents + +- [Server side](#server-side) + - [What is limited](#what-is-limited) + - [Status codes](#status-codes) + - [Configuration](#configuration) + - [Incomplete messages](#incomplete-messages) +- [HTTPS / Kestrel transport](#https--kestrel-transport) +- [Client side](#client-side) +- [Server backpressure signals](#server-backpressure-signals) +- [See also](#see-also) + ## Server side ### What is limited @@ -225,7 +237,14 @@ Pass an `Action` to fully configure the limiter with any `Sy ## Client side -A client that gets a "server busy" signal must not hammer the server with retries — that is exactly what amplifies a connect storm. The default `ReconnectPolicy` (used by `ManagedSession`) is **server-signal-aware**: when the previous attempt failed with an overload signal (`BadServerTooBusy`, `BadTcpServerTooBusy`, `BadTooManySessions`, `BadTooManyOperations`, `BadTooManyPublishRequests`, or a transient timeout) it backs off more aggressively (4× the computed delay, capped at `MaxDelay`) and honors a server-provided retry-after hint as a lower bound. +A client that receives a "server busy" signal must not hammer the server +with retries; that would amplify a connect storm. The default +`ReconnectPolicy` used by `ManagedSession` is **server-signal-aware**. When +an attempt fails with an overload signal—`BadServerTooBusy`, +`BadTcpServerTooBusy`, `BadTooManySessions`, `BadTooManyOperations`, +`BadTooManyPublishRequests`, or a transient timeout—it backs off more +aggressively. It multiplies the computed delay by four, caps it at +`MaxDelay`, and honors any server-provided retry-after hint as a lower bound. This is exposed through `IReconnectPolicy.TryGetNextDelay`, which adapts the backoff to the previous attempt's status code and any server-provided retry-after hint. The connection state machine calls it automatically; a policy that returns `false` opts out of adaptive behavior and the state machine falls back to the basic attempt-based `GetNextDelay`, so a minimal custom policy only has to implement the plain delay. Because a failed initial connect funnels into the same reconnect loop, the adaptive backoff applies to both initial connects and reconnects. diff --git a/docs/RedundancySampleTests.md b/docs/RedundancySampleTests.md index 50736fbe26..c580d8db17 100644 --- a/docs/RedundancySampleTests.md +++ b/docs/RedundancySampleTests.md @@ -1,25 +1,35 @@ # Redundant Sample Integration Tests -The `Opc.Ua.Redundancy.Samples.Tests` project contains process-level integration tests that launch the redundant sample applications -— [`RedundantServer`](../samples/Redundancy/RedundantServer), [`RedundantClient`](../samples/Redundancy/RedundantClient) and -[`RedundantPubSub`](../samples/Redundancy/RedundantPubSub) — as real external processes and assert on the high-availability behavior they log -(`FAILOVER:`, `DATA LOSS:`, `HA OK:`, and reconnect events). They exercise the samples in their supported setups so that regressions in the -end-to-end high-availability experience are caught automatically, and they demonstrate both successful failover *and* the visibility of data loss. +The `Opc.Ua.Redundancy.Samples.Tests` project runs process-level integration +tests against the `RedundantServer`, `RedundantClient`, and `RedundantPubSub` +sample applications. The tests launch the applications as external processes +and assert on logged high-availability events such as `FAILOVER:`, `DATA LOSS:`, +`HA OK:`, and reconnect messages. They cover supported sample configurations, +catch regressions in end-to-end failover, and demonstrate when failover loses +data. -The tests come in a **short-haul** form that runs as part of normal pull-request validation, and a **long-haul** soak form that runs on a -dedicated, manually triggered CI job (and on a weekly schedule) to validate high-availability operation over an extended period. +The test project includes **short-haul** tests for normal pull-request +validation and **long-haul** soak tests for extended runs on dedicated CI jobs. + +## Contents + +- [Short-haul tests](#short-haul-tests-pull-request-validation) +- [Long-haul tests](#long-haul-tests-manual--scheduled-soak) +- [Multi-replica leader-election failover](#multi-replica-leader-election-failover) ## Short-haul tests (pull-request validation) -Short-haul tests are deterministic and complete in seconds. They are discovered and run automatically by the normal -[CI](../.github/workflows/buildandtest.yml) workflow (and the Azure DevOps test stages), because the project follows the -`tests/Opc.Ua.*.Tests` naming convention. They are tagged with the NUnit category `SampleHaShortHaul`: +Short-haul tests are deterministic and complete in seconds. The standard +[CI](../.github/workflows/buildandtest.yml) workflow and Azure DevOps test +stages discover and run them because the project follows the +`tests/Opc.Ua.*.Tests` naming convention. They use the NUnit category +`SampleHaShortHaul`: -* **PubSub demo, hot mode** — runs `RedundantPubSub --role demo --ha-mode hot` and asserts the promoted publisher continues the SequenceNumber +* **PubSub demo, hot mode** — runs `RedundantPubSub --role demo --ha-mode hot` and asserts the promoted publisher continues the SequenceNumber across failover with no reset (`SIMULATED: HA OK: sequence continued ...`). -* **PubSub demo, cold mode** — runs the same demo in cold mode and asserts the SequenceNumber reset (data loss) is made visible +* **PubSub demo, cold mode** — runs the same demo in cold mode and asserts the SequenceNumber reset (data loss) is made visible (`SIMULATED: DATA LOSS: sequence reset ...`). -* **Server + client connectivity** — launches a `RedundantServer` and a `RedundantClient` together and asserts the client connects and begins +* **Server + client connectivity** — launches a `RedundantServer` and a `RedundantClient` together and asserts the client connects and begins high-availability monitoring. Run them locally from the repository root: @@ -30,15 +40,20 @@ dotnet test tests/Opc.Ua.Redundancy.Samples.Tests/Opc.Ua.Redundancy.Samples.Test ## Long-haul tests (manual / scheduled soak) -Long-haul tests are marked `[Explicit]` (NUnit category `SampleHaLongHaul`) so they never run on pull requests. They loop for a configurable -duration and assert the high-availability guarantees hold across many failovers: +Long-haul tests use `[Explicit]` and the NUnit category +`SampleHaLongHaul`, so they do not run on pull requests. They loop for a +configurable duration and check high-availability behavior across many +failovers: -* **PubSub failover soak** — repeatedly runs the PubSub demo in hot and cold modes for the whole duration, asserting the correct continuity +* **PubSub failover soak** — repeatedly runs the PubSub demo in hot and cold modes for the whole duration, asserting the correct continuity (hot) and data-loss (cold) narrative every iteration. -* **Client reconnect soak** — runs a `RedundantClient` against a `RedundantServer` for the whole duration and repeatedly kills and restarts the - server, asserting the client transparently detects the loss, reconnects when the server returns, and never crashes. +* **Client reconnect soak** — runs a `RedundantClient` against a + `RedundantServer` for the entire duration. The test repeatedly stops and + restarts the server, then checks that the client detects the outage, + reconnects when the server returns, and does not crash. -The soak duration is controlled by the `SAMPLE_HA_DURATION_MINUTES` environment variable (default `60`). Run a short local soak with: +The `SAMPLE_HA_DURATION_MINUTES` environment variable controls the soak +duration (default: `60` minutes). Run a short local soak with: ```powershell $env:SAMPLE_HA_DURATION_MINUTES = "5" @@ -47,16 +62,22 @@ dotnet test tests/Opc.Ua.Redundancy.Samples.Tests/Opc.Ua.Redundancy.Samples.Test The long-haul tests run in CI through dedicated, manually triggerable jobs on both platforms: -* **GitHub Actions** — the [Sample HA Long-Haul Test](../.github/workflows/sample-ha-longhaul.yml) workflow (`workflow_dispatch` with a +* **GitHub Actions** — the [Sample HA Long-Haul Test](../.github/workflows/sample-ha-longhaul.yml) workflow (`workflow_dispatch` with a `duration` input, plus a weekly schedule). -* **Azure DevOps** — the [sample-ha-longhaul](../.azurepipelines/sample-ha-longhaul.yml) pipeline (manual run with a `durationMinutes` +* **Azure DevOps** — the [sample-ha-longhaul](../.azurepipelines/sample-ha-longhaul.yml) pipeline (manual run with a `durationMinutes` parameter, plus a weekly schedule). ## Multi-replica leader-election failover -The full multi-replica leader-election topologies — strong (Raft) active/passive and eventual (CRDT gossip) active/active — rely on a -stable virtual endpoint with DNS-based re-resolution to a surviving replica. These are demonstrated end-to-end by the docker-compose setups under -[`samples/Redundancy/RedundantServer`](../samples/Redundancy/RedundantServer) and [`samples/Redundancy/RedundantClient`](../samples/Redundancy/RedundantClient), which run -several replicas on a container network and let you kill the active replica to observe cross-replica failover. The process-level long-haul soak -above focuses on the failover-detection, reconnect, and data-loss-visibility behavior that runs deterministically inside a CI runner without -container networking. +Full multi-replica leader-election topologies include strong (Raft) +active/passive and eventual (CRDT gossip) active/active configurations. They +use a stable virtual endpoint with DNS-based re-resolution to a surviving +replica. + +The Docker Compose setups under +[`samples/Redundancy/RedundantServer`](../samples/Redundancy/RedundantServer) +and [`samples/Redundancy/RedundantClient`](../samples/Redundancy/RedundantClient) +run several replicas on a container network. Stop the active replica to +observe cross-replica failover. The process-level long-haul soak above focuses +on deterministic failover detection, reconnect, and data-loss visibility +without container networking. diff --git a/docs/ReleaseProcess.md b/docs/ReleaseProcess.md index 6c9a8c93b8..f7fb74379d 100644 --- a/docs/ReleaseProcess.md +++ b/docs/ReleaseProcess.md @@ -11,6 +11,25 @@ without any other context. > release branch, backporting a fix, running `release.yml`, or recovering > from a failed release. +## Contents + +- [Terms](#terms) +- [Common preflight](#common-preflight) +- [Which procedure do I need?](#which-procedure-do-i-need) +- [Branch and channel model](#branch-and-channel-model) +- [First stable release](#first-stable-release) +- [Patch release](#patch-release) +- [Minor release](#minor-release) +- [Routine backport](#routine-backport) +- [Emergency hotfix](#emergency-hotfix) +- [Candidate and dry run](#candidate-and-dry-run) +- [Approved promotion](#approved-promotion) +- [Failed or partial release](#failed-or-partial-release) +- [Post-release verification](#post-release-verification) +- [Release handoff template](#release-handoff-template) +- [Historical `release/2.0.0` branch](#historical-release200-branch) +- [Maintaining this document](#maintaining-this-document) + ## Terms - **Root version** - the package version computed from the committed diff --git a/docs/ReplicaNodeIdentity.md b/docs/ReplicaNodeIdentity.md index 22db9abbfb..111540d740 100644 --- a/docs/ReplicaNodeIdentity.md +++ b/docs/ReplicaNodeIdentity.md @@ -10,6 +10,13 @@ value must agree. Built-in server diagnostics and configuration remain local. The module wraps the standard `DefaultNodeIdFactory`; it does not introduce a second hashing algorithm or a persistent logical-key-to-NodeId database. +## Contents + +- [Configure the whole replica set](#configure-the-whole-replica-set) +- [Creating, importing and hydrating nodes](#creating-importing-and-hydrating-nodes) +- [Stored contracts and peer admission](#stored-contracts-and-peer-admission) +- [Sample client proof](#sample-client-proof) + ## Configure the whole replica set Every replica uses the same replica-set ID, ordered namespace list and assignment @@ -66,20 +73,28 @@ instance namespaces before creating nodes, including namespaces needed by their types, references and NodeId-valued data. New shared namespaces require an explicit layout revision, not a different runtime append order. -Validation covers namespace-bearing type definitions, data types, modelling rules, -references and role permissions, plus NodeId/ExpandedNodeId/QualifiedName values -inside arrays, matrices, DataValues and encodeable structures (including method -arguments). Structured values are visited through their normal encoding contract, -without reflection. Register the codec for an opaque ExtensionObject before using -it as shared data; an unknown body cannot be certified as namespace-safe. +Validation checks: -Runtime manager preparation validates the future ownership composition and -hydrates retained identities into the hidden replacement before its routes are -published. After publication, the module rebinds capture and hydration to the new -manager instances. Active/passive retains the shared store and election; -active/active retains the gossip transport and CRDT map. A failed preparation -recovers bindings for the composition the lifecycle retained rather than leaving -replication attached only to retired managers. +- Namespace-bearing type definitions, data types, modelling rules, references, + and role permissions. +- `NodeId`, `ExpandedNodeId`, and `QualifiedName` values inside arrays, + matrices, `DataValue`s, and encodeable structures, including method + arguments. + +The validator visits structured values through their normal encoding contract, +without reflection. Register a codec for an opaque `ExtensionObject` before +using it as shared data; the validator cannot certify an unknown body as +namespace-safe. + +Before publishing a replacement manager, the runtime validates its ownership +composition and hydrates retained identities into it. After publication, the +module rebinds capture and hydration to the new manager instances. Active/passive +reconfiguration retains the shared store and election. Active/active +reconfiguration retains the gossip transport and CRDT map. + +If preparation fails, the runtime restores bindings for the manager composition +that the lifecycle retained. Replication therefore does not remain attached +only to retired managers. Both server startup modules reject missing identity configuration for replicated operation. Process-local active/passive experimentation and ordinary standalone diff --git a/docs/ResourceIsolation.md b/docs/ResourceIsolation.md index 33d2216e68..0ae7673d40 100644 --- a/docs/ResourceIsolation.md +++ b/docs/ResourceIsolation.md @@ -13,6 +13,37 @@ use `OpcUaServerOptions.ResourceIsolation` or the fluent configuration below. **Balanced is the default**, for both direct and hosted `StandardServer` instances. A host can instead supply an `IServerResourceIsolationProvider`. +## Contents + +- [Terms](#terms) +- [Profiles](#profiles) + - [SharedOnly](#sharedonly) + - [FairShare](#fairshare) + - [Balanced (default)](#balanced-default) + - [TrustedReservations](#trustedreservations) +- [Configuration](#configuration) + - [JSON configuration](#json-configuration) + - [Providers and startup ownership](#providers-and-startup-ownership) +- [Startup validation and sizing](#startup-validation-and-sizing) + - [Sessions, channels, and reserved capacity](#sessions-channels-and-reserved-capacity) + - [Reserve sizes and explicit opt-outs](#reserve-sizes-and-explicit-opt-outs) + - [View effective limits at startup](#view-effective-limits-at-startup) + - [Running in a container](#running-in-a-container) + - [Inspect the plan in application code](#inspect-the-plan-in-application-code) +- [Accounting and lifetime](#accounting-and-lifetime) + - [Publish requests that wait for notifications](#publish-requests-that-wait-for-notifications) + - [Letting a custom handler release its worker while waiting](#letting-a-custom-handler-release-its-worker-while-waiting) + - [What happens when a limit is reached](#what-happens-when-a-limit-is-reached) + - [Configuring request and caller limits](#configuring-request-and-caller-limits) + - [Memory for clients that already have a Session](#memory-for-clients-that-already-have-a-session) +- [Trust, anonymous clients and NAT](#trust-anonymous-clients-and-nat) + - [Before a caller is authenticated](#before-a-caller-is-authenticated) + - [After application or Session authentication](#after-application-or-session-authentication) + - [Repeated requests and cached information](#repeated-requests-and-cached-information) + - [Example: dedicated trusted ingress](#example-dedicated-trusted-ingress) +- [Transport behavior and overload signals](#transport-behavior-and-overload-signals) +- [Limitations](#limitations) + ## Terms These terms describe resource accounting, not permission to access OPC UA data. diff --git a/docs/ReverseConnect.md b/docs/ReverseConnect.md index 9e96e6c18b..ca222e1b08 100644 --- a/docs/ReverseConnect.md +++ b/docs/ReverseConnect.md @@ -2,19 +2,36 @@ ## Overview -The Reverse Connect option consists of the following elements: +The stack supports Reverse Connect through these components: -* Updated C# Stack that supports the *ReverseHello* message for Client and Server; -* Updated server library which supports: - * Server-initiated connections through *ReverseConnectServer*, used automatically by the regular dependency-injection server. - * Extended configuration parameters to set up the client location and timeouts. - * An API extension in the *ReverseConnectServer* to programmatically control client connections. -* Updated client library which supports: - * Configure a client endpoint to accept *ReverseHello* messages using a *ReverseConnectManager*. - * A client API extension to allow applications to register for reverse connections either by callback or by waiting for the *ReverseHello* message for a specific server endpoint and application Uri combination. An optional filter for server Uris or endpoint Urls can be applied to allow multiple clients to use the same endpoint. -* The C# [Console Reference Server](../samples/Reference/ConsoleReferenceServer) with reverse connect support in the configuration xml. -* The C# Core [Console Reference Client](../samples/Reference/ConsoleReferenceClient) that can initiate a Reverse connection with command line options. -* A modified C# [Aggregation Server](https://github.com/OPCFoundation/UA-.NETStandard-Samples/tree/master/Workshop/Aggregation) that supports incoming and outgoing reverse connections. +- The C# stack implements the `ReverseHello` message for clients and servers. +- The server library provides server-initiated connections through + `ReverseConnectServer`. The regular dependency-injection server uses this + support automatically. Configuration options control client locations + and timeouts, and the API lets applications manage client connections. +- The client library provides `ReverseConnectManager` to accept + `ReverseHello` messages. Applications can register for connections by + callback or wait for a message matching a server endpoint and application + URI. Optional server-URI and endpoint-URL filters let multiple clients + share one listener. +- The [Console Reference Server](../samples/Reference/ConsoleReferenceServer) + supports reverse connect through its configuration XML. +- The [Console Reference Client](../samples/Reference/ConsoleReferenceClient) + can initiate a reverse connection through command-line options. +- The [Aggregation Server](https://github.com/OPCFoundation/UA-.NETStandard-Samples/tree/master/Workshop/Aggregation) + supports incoming and outgoing reverse connections. + +## Contents + +- [Overview](#overview) +- [Reverse Connect Handshake](#reverse-connect-handshake) +- [Sharing a listener across multiple Servers](#sharing-a-listener-across-multiple-servers) +- [Server-side dependency injection](#server-side-dependency-injection) +- [Dependency-injection lifecycle](#dependency-injection-lifecycle) +- [Configuration Extensions](#configuration-extensions) +- [WSS reverse-connect](#wss-reverse-connect-opcwss) +- [Kestrel-hosted opc.tcp reverse-connect](#kestrel-hosted-opctcp-reverse-connect-opt-in) +- [Known limitations and issues](#known-limitations-and-issues) ## Reverse Connect Handshake @@ -22,19 +39,59 @@ More details on the reverse connect handshake can be found in the OPC UA spec Pa The *ReverseHello* message allows Servers behind firewalls to initiate communication with Clients. This requires that the Server be pre-configured with the location of the Client. The Server adds a configuration option that can be used to initiate a *ReverseHello* with the Client running behind a firewall. -Once the reverse connection is established, the Server will automatically re-establish the connection if it is closed. Most Servers keep sending *ReverseHello* messages, even if the Client is already connected. In this Server implementation the behavior is configurable to keep sending *ReverseHello* messages, to allow only a single connection or to stop sending messages once the maximum number of Server sessions is exceeded. Only for the single connection configuration the sending of messages is suspended for a configurable timeout if the connection is rejected (i.e. the Client returns *BadTcpMessageTypeInvalid* meaning the Client does not support reverse connections or it does not want a connection from this Server at this time). +Once the reverse connection is established, the Server will automatically re-establish the connection if it is closed. Most servers continue sending `ReverseHello` messages after a client connects. +This implementation offers three behaviors: continue sending messages, allow +only one connection, or stop sending after the server reaches its maximum +session count. In single-connection mode, a rejected connection suspends +further messages for a configurable timeout. A client rejects an unsupported +or unwanted connection with `BadTcpMessageTypeInvalid`. -In order to validate and accept a reverse connection in a Client application a *ReverseConnectManager* is configured to call back to the registered applications or to hold incoming connections open for a programmable timeout. An application can register for incoming requests to accept or reject a reverse connection directly or an application can start a connection and wait for an incoming *ReverseHello* message. If the *ReverseConnectManager* holds already an open connection to the Server the connection can be established without waiting. +To accept reverse connections, configure a client `ReverseConnectManager` to +invoke registered callbacks or hold incoming connections open for a +configurable timeout. An application can register a callback to accept or +reject requests, or start a connection and wait for a matching +`ReverseHello`. If the manager already holds an open connection to the +server, the client can establish the session without waiting. -The host port is implemented by a transport which implements the *ITransportListener* interface. The transport calls the *ReverseConnectionManager* class in the client library to provide the application interface. This implementation uses only a single port on a client to support multiple incoming *ReverseHello* server connections. The clients register at the *ReverseConnectionManager* for specific serversUris, endpointUrls or any incoming message for callbacks. When the Client receives a *ReverseHello* the application receives an *ITransportWaitingConnection* connection object which can be used to create a client session in a similar way as by connecting using the endpointUrl, just by using a different Connect API which supports the connection object as a parameter. The client then uses the open socket to send the *Hello* message back to the Server for the well known establishment of a secure communication session. +The client transport implements `ITransportListener` and routes incoming +connections through `ReverseConnectionManager`. One client port can accept +`ReverseHello` messages from multiple servers. Register callbacks for a +specific server URI, endpoint URL, or any incoming connection. -The second option for a client application is to call the Connect API with a configured *ReverseConnectionManager* to wait for an incoming connection and to establish the connection before the timeout expires. This connection model is similar to the standard connect flow with a Server and might be a good model to add reverse connect support for existing applications, without changing the application logic. +When a client receives `ReverseHello`, its callback receives an +`ITransportWaitingConnection`. Pass this connection object to the session +connect API to create a session, much like connecting with an endpoint URL. +The client uses the open socket to send `Hello` back to the server and +continue secure-channel establishment. -If no client responds to the *ReverseHello* message or if it is even rejected, the channel is closed with a *BadTcpMessageTypeInvalid* error which the server should interpret as an indication that the Client is not configured to respond to *ReverseHello* messages of that Server. +Alternatively, call the Connect API with a configured +`ReverseConnectionManager`. The client waits for an incoming connection and +establishes the session if it arrives before the timeout expires. Existing +applications can adopt this pattern without changing their broader +connection flow. -If the Client accepts the connection, a secure connection requires that the Client calls *GetEndpoints* to fetch the Server Certificate. At this point the Client closes the channel, which means it needs to wait for the Server to automatically re-connect. When it does, it can use the security information previously cached to connect securely back to the Server. An optimized Server implementation could respond with an immediate *ReverseHello* message to avoid connection delays after a call to *GetEndpoints* . +If no client responds to `ReverseHello`, or the client rejects it, the +channel closes with `BadTcpMessageTypeInvalid`. The server should interpret +this as an indication that the client is not configured to accept that +server's reverse connections. -The auto-reconnect behavior on the Server is essential to any real application, because Clients close the Socket when the SecureChannel is closed. According to the specification a Server needs to abort the auto-reconnect if it receives a *BadTcpMessageTypeInvalid* code, because that is the error it will receive from peers that have not been upgraded to support the *ReverseHello*. Because of this a Client can use the same error code to tell the Server to stop reconnecting, if a user has rejected the connection. However, in this implementation, only if the Server is configured for a single connection it applies an extended timeout before reconnecting to the Client to reduce the overall traffic. In other configurations the Server keeps sending the *ReverseHello* messages at the configured time interval. +After accepting the connection, a client that needs a secure channel must +call `GetEndpoints` to retrieve the server certificate. The client then +closes that channel and waits for the server to reconnect. On reconnection, +the client can use the cached security information to connect securely. +An optimized server can send `ReverseHello` immediately after `GetEndpoints` +to reduce this delay. + +Server auto-reconnect is essential because clients close the socket when +the SecureChannel closes. The specification requires a server to stop +reconnecting if it receives `BadTcpMessageTypeInvalid`. Older peers that do +not support `ReverseHello` return this error. A client can also return it +when a user rejects the connection. + +In this implementation, a server configured for a single connection waits +for an extended, configurable timeout before sending another `ReverseHello`. +In other modes, the server continues sending `ReverseHello` messages at the +configured interval. ## Sharing a listener across multiple Servers diff --git a/docs/Robotics.md b/docs/Robotics.md index 67b0164603..f67cde7510 100644 --- a/docs/Robotics.md +++ b/docs/Robotics.md @@ -1,13 +1,14 @@ # OPC UA Robotics -The Robotics libraries implement the OPC UA Robotics companion specification -([OPC 40010-1](https://reference.opcfoundation.org/Robotics/v102/docs/), version -1.02). Robotics builds on Industrial Automation -([OPC 10000-200](https://reference.opcfoundation.org/IA/v400/docs/)), which in turn -builds on Device Integration -([OPC 10000-100](https://reference.opcfoundation.org/DI/v104/docs/)); all three -models are source-generated from their released NodeSets and loaded in -dependency order. +The Robotics libraries implement version 1.02 of the OPC UA Robotics +companion specification +([OPC 40010-1](https://reference.opcfoundation.org/Robotics/v102/docs/)). +Robotics builds on Industrial Automation +([OPC 10000-200](https://reference.opcfoundation.org/IA/v400/docs/)), +which builds on Device Integration +([OPC 10000-100](https://reference.opcfoundation.org/DI/v104/docs/)). +The stack generates all three models from their released NodeSets and loads +them in dependency order. > **Status: draft companion model.** The namespace > `http://opcfoundation.org/UA/RobotIntent/` and every NodeId in it are @@ -29,8 +30,61 @@ Robot Intent supplies the verbs, and only the verbs, so the two compose rather t * [`Opc.Ua.Robotics.Client`](../src/Opc.Ua.Robotics.Client) carries discovery, the awaitable operation handle, command authority, missions and the fluent intent builders. -The NodeSet declares exactly one `RequiredModel` — the base OPC UA namespace — so a server can adopt -Robot Intent without pulling in OPC 40010, OPC 10000-100 DI, or anything else. +The NodeSet declares exactly one `RequiredModel`: the base OPC UA namespace. +A server can therefore adopt Robot Intent without depending on OPC 40010, +OPC 10000-100 Device Integration, or another companion model. + +## Contents + +- [Packages](#packages) +- [Minimal hosted server](#minimal-hosted-server) +- [Hosting API](#hosting-api) + - [Class-based code-behind](#class-based-code-behind) + - [Model providers](#model-providers) +- [Build context](#build-context) +- [Topology builders](#topology-builders) +- [Custom node managers and non-DI hosting](#custom-node-managers-and-non-di-hosting) +- [Vendor extensions](#vendor-extensions) +- [Layering](#layering) +- [Common contracts](#common-contracts) +- [Client](#client) + - [Registration](#registration) + - [API](#api) + - [Walking a cell](#walking-a-cell) + - [Classifying a discovered node](#classifying-a-discovered-node) + - [Invoking robot operations](#invoking-robot-operations) + - [Standard operations](#standard-operations) + - [Programs](#programs) + - [Observing telemetry](#observing-telemetry) +- [Operation conventions](#operation-conventions) +- [Sample](#sample) +- [URML primitive mapping](#urml-primitive-mapping) +- [Robot Intent](#robot-intent) + - [Hosting a controller](#hosting-a-controller) + - [Declaring a robot](#declaring-a-robot) + - [Writing an executor](#writing-an-executor) + - [Client: submit and await](#client-submit-and-await) + - [Client: missions](#client-missions) + - [Client: cancellation, pause and retry](#client-cancellation-pause-and-retry) + - [Pose maths](#pose-maths) + - [Handling refusal](#handling-refusal) + - [Monitoring and direct control for agents](#monitoring-and-direct-control-for-agents) + - [MCP tools for Robot Intent](#mcp-tools-for-robot-intent) + - [Facets in code](#facets-in-code) + - [Limitations](#limitations) + - [Why a submission is not a method call](#why-a-submission-is-not-a-method-call) + - [The intent hierarchy](#the-intent-hierarchy) + - [Poses, frames and units](#poses-frames-and-units) + - [The lifecycle](#the-lifecycle) + - [Missions](#missions) + - [Command authority](#command-authority) + - [Safety and the boundary](#safety-and-the-boundary-that-is-never-crossed) + - [What this interface carries and what it brokers](#what-this-interface-carries-and-what-it-brokers) + - [NodeIds in intents are untrusted input](#nodeids-in-intents-are-untrusted-input) + - [Interoperating with OPC 40010](#interoperating-with-opc-40010) + - [Facets](#facets) + - [Profile and facet URI publication](#profile-and-facet-uri-publication) +- [See also](#see-also) ## Packages diff --git a/docs/RoleBasedUserManagement.md b/docs/RoleBasedUserManagement.md index 22ba2cb263..72173fce6d 100644 --- a/docs/RoleBasedUserManagement.md +++ b/docs/RoleBasedUserManagement.md @@ -1,5 +1,21 @@ # Role-Based Security (OPC UA Part 18) +## Contents + +- [Overview](#overview) +- [Server side](#server-side) + - [`IRoleManager`](#irolemanager) + - [Authoring an identity-mapping rule with claims](#authoring-an-identity-mapping-rule-with-claims) + - [`RoleConfigurationChanged` event](#roleconfigurationchanged-event) + - [Typed-proxy address-space binding](#typed-proxy-address-space-binding) + - [Default impersonation flow](#default-impersonation-flow) + - [User Management](#user-management-part-18-5) + - [User name / password storage](#user-name--password-storage) +- [Client side](#client-side) +- [GDS](#gds) +- [Known limitations](#known-limitations) +- [References](#references) + ## Overview The OPC UA .NET Standard Stack implements the role-based security model from @@ -68,7 +84,6 @@ At least one field must be non-default; `AddEndpoint` returns `Bad_InvalidArgume `EndpointType`. > **Identity-claim criteria support**: -> > - `IdentityCriteriaType.GroupId` probes the returned `IUserIdentity` for `Opc.Ua.Identity.IIdentityClaims` (see [Identity Providers](IdentityProviders.md)) and matches `IIdentityClaims.Groups`. > - `IdentityCriteriaType.Role` matches roles asserted **inside the access token** via `IIdentityClaims.Roles`, optionally prefixed by the issuer URI as `iss/roleName`, per Part 18 4.4.4. diff --git a/docs/RuntimeNodeSets.md b/docs/RuntimeNodeSets.md index 1b73a0fd99..c0d0d49950 100644 --- a/docs/RuntimeNodeSets.md +++ b/docs/RuntimeNodeSets.md @@ -1,6 +1,9 @@ # Runtime NodeSets -This guide explains how to load one or more NodeSet2 XML documents into the server's address space at startup without writing a source-generated or hand-coded NodeManager. You configure which files or streams to load; the server imports them in dependency order and registers the resulting nodes. +This guide explains how to import one or more NodeSet2 XML documents into a +server's address space at startup, without writing a source-generated or +hand-coded NodeManager. Configure the files or streams to load. The server +imports them in dependency order and registers the resulting nodes. Within each imported set, types are registered in inheritance order rather than XML record order. A derived type may precede its supertype in the document; @@ -8,6 +11,26 @@ an inheritance cycle is rejected before registration. Imported namespace-zero `InputArguments` and `OutputArguments` Properties populate their Method's typed signature, retaining the declared Property NodeIds, argument order, and values. +## Contents + +- [When to use the runtime NodeSet path](#when-to-use-the-runtime-nodeset-path) +- [Startup and live lifecycle semantics](#startup-and-live-lifecycle-semantics) + - [Shadow reload](#shadow-reload) + - [Immediate reload](#immediate-reload) +- [Quick-start examples](#quick-start-examples) + - [Single file](#single-file) + - [Single file with a fluent callback](#single-file-with-a-fluent-callback) + - [Group of dependent NodeSets](#group-of-dependent-nodesets) + - [Custom stream source](#custom-stream-source) + - [Direct factory registration](#direct-factory-registration) +- [Stream ownership contract](#stream-ownership-contract) +- [Default namespace for unqualified browse paths](#default-namespace-for-unqualified-browse-paths) +- [Dependency sorting](#dependency-sorting) + - [Referencing a Node another NodeManager owns](#referencing-a-node-another-nodemanager-owns) +- [Complex types](#complex-types) +- [Comparison with source-generated NodeManagers](#comparison-with-source-generated-nodemanagers) +- [Related documentation](#related-documentation) + ## When to use the runtime NodeSet path Use `AddRuntimeNodeSet` when: @@ -90,7 +113,10 @@ The rules that apply to every NodeManager registered at runtime -- what happens ### Shadow reload -`ShadowReloadRuntimeNodeSetAsync` (backed by `INodeManagerLifecycle.ShadowReloadAsync`) replaces a live registration the same way `ReloadRuntimeNodeSetAsync` does, but without the active-monitored-item guard: +`ShadowReloadRuntimeNodeSetAsync` permits replacing a generation that still owns +active monitored items. Those items stay on the retired generation and continue +to be served there until they are deleted or otherwise released; new requests use +the replacement generation. Normal reload instead migrates compatible monitored items. ```csharp public async ValueTask ShadowReloadAsync(CancellationToken ct) @@ -105,18 +131,19 @@ public async ValueTask ShadowReloadAsync(CancellationToken ct) } ``` -The replacement generation is prepared and published through the same transactional prepare/publish/commit/rollback path as `ReloadAsync`, so a failure during preparation, publication, or the routing switch leaves the current generation fully active and cleans up the replacement, exactly as a normal reload does. Once committed, every new service request is atomically routed to the replacement generation, including for namespaces the current and replacement generations share. - -The current generation is not torn down immediately. It is moved to the same retired-generation bookkeeping used for an ordinary reload, but its existing monitored items and any request or continuation point that already captured it keep being served by it, unaffected by the routing switch. The retired generation is disposed automatically, without deleting any client subscription, once its monitored items and in-flight state drain; a later lifecycle operation (or shutdown) opportunistically retries that cleanup until it succeeds. `ShadowReloadAsync` returns the replacement `NodeManagerRegistration` immediately and invalidates the current handle for further lifecycle mutations, the same as `ReloadAsync`. - -Use `ShadowReloadAsync` when a model update must take effect for new requests without waiting for existing subscriptions to unsubscribe first; use the fail-closed `ReloadAsync` when a stale generation must never remain reachable, even briefly, for already-open monitored items. +The returned registration replaces the old handle for lifecycle operations. +Choose shadow reload only when retaining the old model for existing subscriptions +is intentional; its resources remain allocated until those subscriptions drain. ### Immediate reload -`ImmediateReloadRuntimeNodeSetAsync` (backed by `INodeManagerLifecycle.ImmediateReloadAsync`) performs the same atomic replacement but does not retain the previous generation until monitored items drain. After requests that already captured the old routing generation finish, every affected data-change monitored item is made publishable with `BadNodeIdUnknown`, event monitored items stop producing events, continuation points are invalidated, and the old NodeManager is disposed. The subscription and monitored-item records remain available so clients can receive the status and delete or recreate the affected items. - -Use immediate reload only when continuity through the previous generation is not required. Durable monitored items are not eligible for immediate retirement because their terminal state would have to survive restart; choose shadow reload for any generation that owns them. +`ImmediateReloadRuntimeNodeSetAsync` does not migrate existing monitored items. +Affected data-change items receive `BadNodeIdUnknown`, and clients may recreate +items against the replacement model. +The [node-manager reload matrix](NodeManagers.md#reload-modes) is authoritative for +all three modes, including request draining, continuation points, and generation +cleanup. Runtime NodeSet reload methods use those same lifecycle operations. ## Quick-start examples diff --git a/docs/samples.md b/docs/Samples.md similarity index 84% rename from docs/samples.md rename to docs/Samples.md index 0b45b7b8fe..a526250b78 100644 --- a/docs/samples.md +++ b/docs/Samples.md @@ -3,7 +3,30 @@ This repository contains a large collection of platform-independent samples that demonstrate the stack's client, server, PubSub, companion-model, and developer-tooling APIs. Each sample has its own `README.md` with build and run -instructions. +instructions. If you are new to the SDK, start with the +[minimal samples](#minimal-samples). + +## Contents + +- [Minimal samples](#minimal-samples) +- [Reference applications](#reference-applications) +- [PubSub samples](#pubsub-samples) +- [Companion-model samples](#companion-model-samples) +- [OpenUSD site composition](#openusd-site-composition) + +## Minimal samples + +[Getting started](GettingStarted.md) runs the Minimal Boiler Server and the +Minimal Client together. + +- [Minimal Boiler Server](../samples/MinimalApi/MinimalBoilerServer) — minimal + Boiler-model server with the fluent state-machine builder; Native-AOT + publishable. +- [Minimal Client](../samples/MinimalApi/MinimalClient/README.md) — hosted + client that discovers a secure endpoint of the Minimal Boiler Server, creates + a subscription, browses, and reads; Native-AOT publishable. +- [Minimal Calc Server](../samples/MinimalApi/MinimalCalcServer) — minimal + server built on the source-generated NodeManager pipeline and Calc model. ## Reference applications @@ -23,13 +46,8 @@ instructions. one executable with `publisher`, `subscriber`, and `external` external-server-adapter modes across the supported transport profiles. -## Minimal and companion-model samples +## Companion-model samples -- [Minimal Calc Server](../samples/MinimalApi/MinimalCalcServer) — minimal - server built on the source-generated NodeManager pipeline and Calc model. -- [Minimal Boiler Server](../samples/MinimalApi/MinimalBoilerServer) — minimal - Boiler-model server with the fluent state-machine builder; Native-AOT - publishable. - [Pump Device Integration Server](../samples/DI/PumpDeviceIntegrationServer/README.md) — Device Integration Part 100 server using `Opc.Ua.Di.Server`'s fluent builder. diff --git a/docs/SchemaGeneration.md b/docs/SchemaGeneration.md index 10c5050db7..27e5820552 100644 --- a/docs/SchemaGeneration.md +++ b/docs/SchemaGeneration.md @@ -6,7 +6,21 @@ The `Opc.Ua.Core.Schema` library generates schemas for OPC UA data types at runt - **BSD** (OPC Binary, Part 6) for the binary encoding. - **JSON Schema** (Part 6 Annex C, draft 2020-12) for the JSON encoding, in both the **compact** (reversible, BrowseName-keyed) and **verbose** flavors. -Schemas are built as strongly-typed object models in code — there are no embedded schema strings — so unused generation paths are trimmed away and the whole library is NativeAOT compatible. The XSD object model is the in-box `System.Xml.Schema.XmlSchema`, the BSD object model is the existing `Opc.Ua.Schema.Binary.TypeDictionary`, and the JSON object model is `System.Text.Json.Nodes.JsonObject`. +Schemas are built as strongly-typed object models in code — there are no embedded schema strings — so unused generation paths are trimmed away and the whole library is NativeAOT compatible. The XSD object model is the in-box `System.Xml.Schema.XmlSchema`. BSD uses +the existing `Opc.Ua.Schema.Binary.TypeDictionary`, and JSON uses +`System.Text.Json.Nodes.JsonObject`. + +## Contents + +- [Concepts](#concepts) +- [Registration](#registration) +- [Registering data types](#registering-data-types) + - [Namespace identity](#namespace-identity) +- [Generating a schema](#generating-a-schema) +- [Working with the object model](#working-with-the-object-model) +- [JSON encoding notes](#json-encoding-notes-part-6) +- [PubSub schemas](#pubsub-schemas) +- [Trimming and NativeAOT](#trimming-and-nativeaot) ## Concepts @@ -58,11 +72,12 @@ var registry = serviceProvider.GetRequiredService(); registry.TryAddDataType(dataTypeNode, session.NamespaceUris); ``` -- **Source-generated types** — the generated model registration extension adds public - structure and enumeration activators to an `IEncodeableFactory`. Those activators - implement `IDataTypeDefinitionSource`; the generated data value itself does not need - to implement that interface. Use `EncodeableFactoryDefinitionSource` to resolve their - definitions without reflection or manual registration: +- **Source-generated types** — the generated model registration extension adds + public structure and enumeration activators to an `IEncodeableFactory`. + These activators implement `IDataTypeDefinitionSource`; the generated data + value does not need to implement that interface. Use + `EncodeableFactoryDefinitionSource` to resolve their definitions without + reflection or manual registration: ```csharp IEncodeableFactory factory = EncodeableFactory.Create(); @@ -91,7 +106,9 @@ IDataTypeDefinitionResolver resolver = new CompositeDataTypeDefinitionResolver( [new EncodeableFactoryDefinitionSource(factory, namespaceUris), registry]); ``` - An OPC UA server wires this up automatically when `AddComplexTypeSystem()` is enabled (see [ComplexTypes.md](ComplexTypes.md)): the primed encodeable factory becomes the schema resolver. + When `AddComplexTypeSystem()` is enabled, an OPC UA server wires this up + automatically (see [ComplexTypes.md](ComplexTypes.md)). The primed + encodeable factory becomes the schema resolver. Once registered, fields that reference other registered types are resolved automatically and included in the generated document. diff --git a/docs/ServerScalability.md b/docs/ServerScalability.md index 7574f19c8c..d58d6baf82 100644 --- a/docs/ServerScalability.md +++ b/docs/ServerScalability.md @@ -2,6 +2,17 @@ This document describes how the reference server scales to large numbers of concurrent client sessions on a single node, what bounds that scale, the built-in controls for degrading gracefully under load, and how to configure the server and scale out. For measured numbers see [Performance Benchmarks — Server session scalability](Benchmarks.md#server-session-scalability). +## Contents + +- [How a session is established](#how-a-session-is-established) +- [What bounds single-node scale](#what-bounds-single-node-scale) +- [Admission control and rate limiting](#admission-control-and-rate-limiting) +- [Held Publishes and the request-thread budget](#held-publishes-and-the-request-thread-budget) +- [Session diagnostics cost](#session-diagnostics-cost) +- [Configuration](#configuration) +- [Scaling out](#scaling-out) +- [See also](#see-also) + ## How a session is established A client session is brought up in four stages: @@ -35,9 +46,22 @@ Session establishment keeps the CPU-bound signature work outside the session-tab ## Held Publishes and the request-thread budget -A steady-state session keeps one or more long-poll `Publish` requests outstanding, each waiting for the next notification. How many a client keeps outstanding depends on its publish-pipelining strategy — the classic subscription engine deliberately queues several per session to smooth delivery. The operating-system thread is released while each waits. With **`DecoupleHeldPublishRequests`** enabled (the default), each parked `Publish` also releases its request-processing worker at the point it parks — independently, so a session holding several parked Publishes releases a worker for each — so a small worker pool can hold many thousands of outstanding Publishes across sessions and **`MaxRequestThreadCount`** does not have to scale with the session or publish count. +A steady-state session keeps one or more long-poll `Publish` requests +outstanding. Each waits for the next notification. The number depends on the +client's publish-pipelining strategy; the classic subscription engine queues +several per session to smooth delivery. -Setting `DecoupleHeldPublishRequests` to `false` restores the behavior where each held `Publish` occupies a worker for the duration of its wait; a server serving N sessions then needs `MaxRequestThreadCount` well above N to avoid starving other requests. +The operating-system thread is released while a `Publish` waits. By default, +`DecoupleHeldPublishRequests` also releases the request-processing worker when +the request parks. Each parked request releases its worker, so a session with +several outstanding Publishes releases several workers. A small pool can +therefore support many thousands of parked Publishes without scaling +`MaxRequestThreadCount` to the number of sessions or Publishes. + +Set `DecoupleHeldPublishRequests` to `false` to restore the legacy behavior. +Each held `Publish` then occupies a worker for the duration of its wait. A +server serving N sessions needs `MaxRequestThreadCount` well above N to avoid +starving other requests. ## Session diagnostics cost diff --git a/docs/Sessions.md b/docs/Sessions.md index c119bf4a4f..973bb494bb 100644 --- a/docs/Sessions.md +++ b/docs/Sessions.md @@ -6,6 +6,13 @@ UA session: the raw `Session` and its reconnect helper, the `ManagedSession` facade, the pluggable subscription engines, and how to choose between them. +> **New to the client API?** Run the client in +> [Getting started](GettingStarted.md) first. For new applications, read +> [`ManagedSession`](#3-managedsession--the-connection-state-machine-facade) +> and [Putting it all together](#7-putting-it-all-together). Sections 1 and 2 +> describe the raw `Session` and `SessionReconnectHandler` for applications +> that manage the connection lifecycle themselves. + ## Quick reference | Type | Creates | Reconnect | Subscription engine | Recommended for | @@ -21,14 +28,55 @@ choose between them. Everything below explains how these pieces fit together. +## Contents + +- [Quick reference](#quick-reference) +- [1. `Session` — the OPC UA session primitive](#1-session--the-opc-ua-session-primitive) + - [Operation limits and `MaxArrayLength`](#operation-limits-and-maxarraylength) + - [Plugging in a subscription engine](#plugging-in-a-subscription-engine) + - [`DefaultSessionFactory`](#defaultsessionfactory) +- [2. `SessionReconnectHandler` — legacy reconnect driver](#2-sessionreconnecthandler--legacy-reconnect-driver) + - [Supported session types](#supported-session-types) +- [3. `ManagedSession` — the connection-state-machine facade](#3-managedsession--the-connection-state-machine-facade) + - [Failure of the initial connect](#failure-of-the-initial-connect) + - [Recovery after an established connection is exhausted](#recovery-after-an-established-connection-is-exhausted) + - [`ManagedSessionFactory`](#managedsessionfactory) + - [`ManagedSessionBuilder`](#managedsessionbuilder) + - [Reconnect semantics on `ManagedSession`](#reconnect-semantics-on-managedsession) + - [Closing a `ManagedSession`](#closing-a-managedsession) + - [Server retry-after backpressure](#server-retry-after-backpressure) +- [4. `IClientChannelManager` — centralised channel sharing and reconnect](#4-iclientchannelmanager--centralised-channel-sharing-and-reconnect) + - [Session factory choices](#session-factory-choices) + - [Channel identity (`ManagedChannelKey`)](#channel-identity-managedchannelkey) + - [State model](#state-model) + - [Participant model — `IReconnectParticipant`](#participant-model--ireconnectparticipant) + - [Retry policy — `IChannelReconnectPolicy`](#retry-policy--ichannelreconnectpolicy) + - [HTTPS resilience vs channel-mgr reconnect](#https-resilience-vs-channel-mgr-reconnect) + - [HTTPS factory + OPC UA cert validation: secure-by-default fallback](#https-factory--opc-ua-cert-validation-secure-by-default-fallback) + - [Shared retry budget with `ManagedSession`](#shared-retry-budget-with-managedsession) + - [Diagnostics surface contract — what tags and structured log fields carry](#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) + - [DI registration](#di-registration) + - [Migrating from `AttachChannel` / `DetachChannel`](#migrating-from-attachchannel--detachchannel) + - [Testing the channel manager](#testing-the-channel-manager) +- [5. Subscription engines](#5-subscription-engines) + - [`ClassicSubscriptionEngine`](#classicsubscriptionengine) + - [`DefaultSubscriptionEngine` (V2)](#defaultsubscriptionengine-v2) + - [Unbounded monitored items (default)](#unbounded-monitored-items-default) + - [V2 notification pooling (opt-in)](#v2-notification-pooling-opt-in) + - [Server-side request/response pooling](#server-side-requestresponse-pooling) + - [Choosing an engine](#choosing-an-engine) +- [6. The `INodeCache` surface](#6-the-inodecache-surface) +- [7. Putting it all together](#7-putting-it-all-together) +- [Server session lifecycle](#server-session-lifecycle) +- [See also](#see-also) + ## 1. `Session` — the OPC UA session primitive -`Session` (`src/Opc.Ua.Client/Session/Session.cs`) is the lowest-level -client object that maps directly to a UA secure-channel + session pair on -the server. It implements `ISession` and exposes the full surface of the -OPC UA service set (Read, Write, Browse, Call, AddNodes, etc.) plus -session-level concerns: keep-alive, namespace tables, the type tree, the -node cache, and a publish pipeline. +`Session` (`src/Opc.Ua.Client/Session/Session.cs`) is the lowest-level client +object. It maps directly to a UA SecureChannel and Session on the server. +`Session` implements `ISession` and exposes OPC UA services such as Read, +Write, Browse, Call, and AddNodes. It also manages keep-alive, namespace +tables, the type tree, the node cache, and the publish pipeline. A `Session` is bound to: @@ -346,8 +394,15 @@ connection state machine transitions `Connected → Reconnecting` and: session against the failover endpoint. 5. On success, transitions `Reconnecting → Connected` and releases the service gate, replaying any deferred calls. -6. On exhaustion (`MaxRetries` reached), transitions to `Closed` and - surfaces a `ServiceResultException` to outstanding callers. +6. If the retry budget and failover are exhausted after an established + connection, reports `Disconnected` and the last error, then schedules a new + bounded cycle after the policy's maximum backoff (at least one second). + Closing or disposing the session stops recovery. + +A failed initial connection has a different lifetime: `CreateAsync` disposes the +half-built session and throws when its policy is exhausted. See +[initial-connect failure](#failure-of-the-initial-connect) and +[established-session recovery](#recovery-after-an-established-connection-is-exhausted). When a `ManagedSession` is backed by `IClientChannelManager`, participant reactivation uses the manager's `IChannelReconnectPolicy`. The default `ExponentialBackoffChannelReconnectPolicy` @@ -909,7 +964,7 @@ bound. Use `.WithChannelReconnectTimeout(Timeout.InfiniteTimeSpan)` to opt out o the participant-imposed bound. Explicit caller budgets still apply. For migration notes on the budget-aware APIs, see -[the migration guide](MigrationGuide.md#shared-reconnect-budget-for-managedsession-and-the-channel-manager). +[the session migration guide](migrate/2.0.x/sessions-subscriptions.md#managedsession-and-automatic-reconnection). ### Diagnostics surface contract — what tags and structured log fields carry @@ -979,13 +1034,17 @@ services.AddOpcUa() }; }); -// Single channel manager + multiple sessions sharing channels per endpoint: -var sp = services.BuildServiceProvider(); -var managedFactory = sp.GetRequiredService>>(); -ManagedSession s1 = await managedFactory(ct); -ManagedSession s2 = await managedFactory(ct); // shares s1's underlying channel +// Each call creates a distinct, caller-owned session using the shared channel manager: +await using var sp = services.BuildServiceProvider(); +var managedFactory = sp.GetRequiredService(); +await using ManagedSession s1 = await managedFactory.ConnectAsync(endpoint, ct); +await using ManagedSession s2 = await managedFactory.ConnectAsync(endpoint, ct); ``` +The `Func>` registration is different: it +caches one connected session. Repeated awaits return that same instance; use it +for a single fixed endpoint, not to create independently owned sessions. + ### Migrating from `AttachChannel` / `DetachChannel` `IClientBase.AttachChannel(ITransportChannel)` and `DetachChannel()` diff --git a/docs/SoftwareUpdate.md b/docs/SoftwareUpdate.md index a048f81d89..160591eb89 100644 --- a/docs/SoftwareUpdate.md +++ b/docs/SoftwareUpdate.md @@ -8,6 +8,24 @@ This document explains how to expose the OPC 10000-100 §10.3 software-update facet on a Device Integration (DI) device, what address-space surface it creates, and how clients drive it. +## Contents + +- [Address-space layout](#address-space-layout) +- [Server side — fluent surface](#server-side--fluent-surface) + - [Method-handler hooks](#method-handler-hooks) + - [Server-side state reporting](#server-side-state-reporting) +- [Storage abstractions](#storage-abstractions) + - [Package-store contract](#package-store-contract) + - [File-system layout and provider composition](#file-system-layout-and-provider-composition) + - [Seeding a package](#seeding-a-package) +- [File-transfer pipeline](#file-transfer-pipeline) +- [Client side](#client-side) + - [Uploading a package](#uploading-a-package) + - [Typed Part 16 state-machine surface](#typed-part-16-state-machine-surface) +- [Hosted-server walkthrough](#hosted-server-walkthrough) +- [Implementation pointers](#implementation-pointers) +- [Spec references](#spec-references) + ## Address-space layout `WithSoftwareUpdate(...)` materialises one @@ -34,9 +52,14 @@ address-space surface it creates, and how clients drive it. └─ Confirm (Method) ``` -The Loading subtype is configurable: `UsePackageLoading()` (default, -file transfer + `CloseAndCommit`), `UseDirectLoading()`, -`UseCachedLoading()`. The full structure is added to the +Choose the loading subtype with one of these methods: + +- `UsePackageLoading()` (default), which uses file transfer and + `CloseAndCommit`. +- `UseDirectLoading()`. +- `UseCachedLoading()`. + +The full structure is added to the `AsyncCustomNodeManager`'s `PredefinedNodes` via `AddPredefinedNodeAsync`, so direct NodeId lookup, browse, subscription wiring, and method calls all work out of the box. @@ -91,10 +114,9 @@ device.WithSoftwareUpdate(packageStore, su => su })); ``` -`ISoftwareUpdateContext` exposes the device NodeId, the server's -system context, the package store, and the per-device software -folder so handlers can persist version metadata without having to -re-resolve any of these. +`ISoftwareUpdateContext` exposes the device NodeId, server system context, +package store, and per-device software folder. Handlers can use these values +to persist version metadata without resolving them again. ### Server-side state reporting @@ -125,9 +147,9 @@ device.WithSoftwareUpdate(packageStore, su => su })); ``` -Each hook fires twice per method call — `Started` before the -application callback runs, then `Completed` on success or `Failed` -(with the exception message) on failure. The hook is invoked from the +Each method call raises two hook events. `Started` fires before the +application callback. On success, the method raises `Completed`; on failure, +it raises `Failed` with the exception message. The hook runs in the service call's async context; exceptions thrown by the hook are logged and swallowed so instrumentation faults never abort the underlying SU operation. Domain-keyed `SoftwareUpdatePhase` (rather @@ -165,6 +187,77 @@ Default implementations: | `MemorySoftwareFolder` | Default for `WithSoftwareUpdate`. | | `FileSystemSoftwareFolder` | Persistence across server restarts. | +### Package-store contract + +`ISoftwarePackageStore` implementations must support concurrent calls. +The interface exposes the following asynchronous operations: + +| Operation | Result | +| --- | --- | +| `ListAsync` | Enumerate package metadata. | +| `GetAsync` | Return the matching `SoftwarePackage`, or null if absent. | +| `ExistsAsync` | Report whether an id is present. | +| `OpenReadAsync` | Return a caller-owned payload stream; throw `FileNotFoundException` for an unknown id. | +| `AddAsync` | Copy the supplied stream and add or replace the package with that id. | +| `DeleteAsync` | Return whether a package was removed. | + +`SoftwarePackage` carries `Id`, `Version`, `Vendor`, `Description`, `SizeBytes`, +`CreatedAt`, and an optional `Hash`. Both in-box stores compute `SizeBytes` and +`CreatedAt` during `AddAsync`; callers may supply zero and `default` for those +fields. They do not compute a content hash from an empty `Hash`. +See [`ISoftwarePackageStore`](../src/Opc.Ua.Di.Server/SoftwareUpdate/ISoftwarePackageStore.cs) +for the exact signatures. + +### File-system layout and provider composition + +`FileSystemPackageStore` stores each package beneath a provider-relative root: + +```text +{root}/ + {package-id}/ + payload.bin + metadata.json +``` + +The JSON metadata uses the source-generated `SoftwarePackageJsonContext`, so +serialization does not require reflection. Package ids must be nonempty, must +not contain `/` or `\`, and must not be `.` or `..`; invalid ids throw +`ArgumentException`. + +Reuse the server's configured `IFileSystemProvider`: + +```csharp +ISoftwarePackageStore store = new FileSystemPackageStore( + provider: fileSystemProvider, + rootPath: "/SoftwarePackages"); +``` + +The provider must permit writes for add/delete operations. A read-only provider +can still serve list, metadata, and payload-read operations. The same provider can +also be mounted through `FileSystemNodeManager`; use the store API rather than +editing package files directly. + +### Seeding a package + +Resolve `ISoftwarePackageStore` from the host, then copy the payload into it. +The caller owns and disposes the source stream: + +```csharp +ISoftwarePackageStore store = serviceProvider.GetRequiredService(); +using FileStream payload = File.OpenRead("firmware.bin"); +SoftwarePackage package = await store.AddAsync( + new SoftwarePackage( + Id: "firmware-1.0.0", + Version: "1.0.0", + Vendor: "Acme", + Description: "Device firmware", + SizeBytes: 0, + CreatedAt: default, + Hash: string.Empty), + payload, + ct); +``` + ## File-transfer pipeline When `UsePackageLoading()` is selected (the default), the SU facet diff --git a/docs/SourceGeneratedDataTypes.md b/docs/SourceGeneratedDataTypes.md index 217aed9d8f..5ff73cca40 100644 --- a/docs/SourceGeneratedDataTypes.md +++ b/docs/SourceGeneratedDataTypes.md @@ -5,6 +5,43 @@ implements the `IEncodeable` interface for annotated POCO classes and enums. Thi eliminates the need to hand-write `Encode`, `Decode`, `IsEqual`, and `Clone` methods for custom OPC UA data types. +## Contents + +- [Quick Start](#quick-start) +- [Prerequisites](#prerequisites) + - [Project Reference](#project-reference-internal-development) + - [NuGet Package](#nuget-package-external-consumers) +- [The `DataType` Attribute](#the-datatype-attribute) + - [Properties](#properties) + - [Namespace Resolution Order](#namespace-resolution-order) +- [The `DataTypeField` Attribute](#the-datatypefield-attribute) + - [Properties](#properties-1) + - [Field Selection Rules](#field-selection-rules) +- [Supported Property Types](#supported-property-types) + - [Collections](#collections) + - [Enums and `IEncodeable` Types](#enums-and-iencodeable-types) + - [Unsupported Types](#unsupported-types) +- [Class Variants](#class-variants) + - [Regular Partial Class](#regular-partial-class) + - [Sealed Partial Class](#sealed-partial-class) + - [Record Class](#record-class) + - [Derived Class](#derived-class-inheritance) + - [Internal Class](#internal-class) +- [Enum Support](#enum-support) +- [Registering Types with the Encodeable Factory](#registering-types-with-the-encodeable-factory) +- [Complete Example](#complete-example) +- [`StructureHandling` Enum](#structurehandling-enum) +- [`DefaultValueHandling` Enum](#defaultvaluehandling-enum) + - [How It Works](#how-it-works) + - [Example: Configuration with Defaults](#example-configuration-with-defaults) +- [Partial Init Properties](#partial-init-properties) + - [Example](#example) + - [How It Works](#how-it-works-1) + - [When to Use](#when-to-use) +- [Requirements and Constraints](#requirements-and-constraints) +- [Generated File Output](#generated-file-output) +- [MSBuild Configuration](#msbuild-configuration) + ## Quick Start 1. Mark your class as `partial` and decorate it with `[DataType]`. @@ -55,15 +92,24 @@ source generator project and import its props file: ### NuGet Package (external consumers) -Reference the `OPCFoundation.NetStandard.Opc.Ua.SourceGeneration` package: +Add the `OPCFoundation.NetStandard.Opc.Ua.SourceGeneration` package: + +```bash +dotnet add package OPCFoundation.NetStandard.Opc.Ua.SourceGeneration --prerelease +``` + +The package is a development dependency, so the command adds a reference that +runs the generator at build time and does not flow to consumers of your +project: ```xml + Version="..."> + runtime; build; native; contentfiles; analyzers; buildtransitive + all + ``` diff --git a/docs/StateMachines.md b/docs/StateMachines.md index 44be7b6d43..d39e890080 100644 --- a/docs/StateMachines.md +++ b/docs/StateMachines.md @@ -7,6 +7,36 @@ proxies and the existing `FiniteStateMachineState` server base. For the formal model, see [OPC UA Part 16 — State Machines](https://reference.opcfoundation.org/specs/OPC-10000-16/full). +## Contents + +- [Two-mode unified builder](#two-mode-unified-builder) +- [Quick reference](#quick-reference) +- [Client side](#client-side) + - [Read the current state](#read-the-current-state) + - [Stream transitions](#stream-transitions) + - [Wait for a target state](#wait-for-a-target-state) + - [Enumerate states + transitions](#enumerate-states--transitions) + - [Alarm shelving alignment](#alarm-shelving-alignment) + - [Device Integration (DI) software-update alignment](#device-integration-di-software-update-alignment) + - [Vendor extensibility](#vendor-extensibility) +- [Server side — definition mode](#server-side--definition-mode) +- [Server side — lifecycle mode](#server-side--lifecycle-mode) + - [Stack-shipped state machine](#stack-shipped-state-machine) + - [Inside a fluent node-manager build pipeline](#inside-a-fluent-node-manager-build-pipeline) + - [Lifecycle ordering](#lifecycle-ordering) +- [How the source-generator integrates](#how-the-source-generator-integrates) +- [Guard clauses (fluent sugar)](#guard-clauses-fluent-sugar) +- [Sub-state machines (hierarchical state)](#sub-state-machines-hierarchical-state) + - [Server side — `WithSubStateMachine`](#server-side--withsubstatemachine) + - [Server side — materialized state and transition nodes](#server-side--materialized-state-and-transition-nodes) + - [Executable causes](#executable-causes) + - [Client side — sub-SM observation](#client-side--sub-sm-observation) + - [Client side — typed sub-SM accessors (generated)](#client-side--typed-sub-sm-accessors-generated) + - [Fluent builder behavior — `HasSubStateMachine` is on the state node](#fluent-builder-behavior--hassubstatemachine-is-on-the-state-node) +- [Extensibility recipes](#extensibility-recipes) +- [Tests](#tests) +- [See also](#see-also) + ## Two-mode unified builder The server side ships **one** fluent builder — `StateMachineBuilder` — diff --git a/docs/Subscriptions.md b/docs/Subscriptions.md index fcabcc9285..83d0d04cfa 100644 --- a/docs/Subscriptions.md +++ b/docs/Subscriptions.md @@ -18,17 +18,6 @@ stack: abstraction on top of the V2 engine for state-machine waits and short-lived monitoring. -On the server, newly created monitored items remain provisional until -the subscription rechecks ownership and deletion after node-manager -creation. If transfer, session detachment, deletion, or cancellation wins, -normal node-manager deletion releases those unattached items. - -Publish, queued Publish, Republish, and persistence consumers receive -deep notification snapshots. Acknowledgement, eviction, or queue disposal -can recycle the retained pooled graph without changing a response still -waiting for encoding or a captured retransmission/durable snapshot. -Pooling remains enabled for the retained notifications. - **Recommendation:** new code should use the V2 surface (`ISubscriptionManager` for long-lived application subscriptions, `IStreamingSubscription` for short-lived / await-until-X scenarios). @@ -39,14 +28,24 @@ active, so existing classic-API code keeps working alongside V2. For the full engine comparison (publish-pipeline ownership, worker pools, when to pick which) see -[Sessions.md §4](Sessions.md#4-subscription-engines). +[Sessions: subscription engines](Sessions.md#5-subscription-engines). + +## Contents - [Quick reference](#quick-reference) +- [Streaming subscriptions](#streaming-subscriptions) + - [Obtaining an `IStreamingSubscription`](#obtaining-an-istreamingsubscription) + - [Subscribing to data changes](#subscribing-to-data-changes) + - [Subscribing to events](#subscribing-to-events) + - [Composing streams](#composing-streams) + - [Lifecycle, cancellation, disposal](#lifecycle-cancellation-disposal) + - [Pairing with `AlarmClient`](#pairing-with-alarmclient) - [Server retransmission queues](#server-retransmission-queues) - [Publishing during session recovery](#publishing-during-session-recovery) - [Triggering (SetTriggering)](#triggering-settriggering) - [Declarative triggering](#declarative-triggering) - [Imperative triggering](#imperative-triggering) + - [Cancellation semantics](#cancellation-semantics) - [N:M relationships](#nm-relationships) - [Save / load / restore behavior](#save--load--restore-behavior) - [Error handling](#error-handling) @@ -58,13 +57,6 @@ pools, when to pick which) see - [Durable subscriptions](#durable-subscriptions) - [Performance](#performance) - [Limitations and roadmap](#limitations-and-roadmap) -- [Streaming subscriptions](#streaming-subscriptions) - - [Obtaining an `IStreamingSubscription`](#obtaining-an-istreamingsubscription) - - [Subscribing to data changes](#subscribing-to-data-changes) - - [Subscribing to events](#subscribing-to-events) - - [Composing streams](#composing-streams) - - [Lifecycle, cancellation, disposal](#lifecycle-cancellation-disposal) - - [Pairing with `AlarmClient`](#pairing-with-alarmclient) - [Classic → V2 surface mapping](#classic--v2-surface-mapping) - [Subscription lifecycle](#subscription-lifecycle) - [Notifications and callbacks](#notifications-and-callbacks) @@ -74,7 +66,9 @@ pools, when to pick which) see - [Persistence (save / load)](#persistence-save--load) - [Tuning knobs](#tuning-knobs) - [Engine wiring](#engine-wiring) + - [Per-item options mapping](#per-item-options-mapping) - [Three-API summary](#three-api-summary) +- [Reference](#reference) ## Quick reference @@ -98,8 +92,259 @@ pools, when to pick which) see | Stream — buffer first N | `.BufferedAsync(count)` | | Stream — typed alarms | `streaming.SubscribeAlarmsAsync(notifierId, filter?)` | +## Streaming subscriptions + +`IStreamingSubscription` exposes OPC UA subscription notifications as +`IAsyncEnumerable` streams. Each `SubscribeXxxAsync` call adds a +monitored item to a shared, lazy-created OPC UA subscription and pipes +notifications through a `System.Threading.Channels.Channel`. +Disposing the enumerator removes the monitored item. + +This API sits on top of the V2 subscription engine +(`src/Opc.Ua.Client/Subscription`). It is **not** a replacement +for either the classic or V2 callback-based API — it is a thin +abstraction targeted at three concrete client scenarios: + +- **State-machine waits** — "subscribe, observe transitions until X, + unsubscribe". +- **Short-lived monitoring** — "I need a sample for 30 seconds". +- **Typed alarm streaming** — Part 9 alarm records via + `AlarmStreamExtensions` (see [AlarmsAndConditions.md](AlarmsAndConditions.md)). + +For long-lived application subscriptions, the callback-based +`ISubscriptionManager.Add` + `ISubscriptionNotificationHandler` API +remains the right choice (see +[Sessions: DefaultSubscriptionEngine (V2)](Sessions.md#defaultsubscriptionengine-v2)). + +### Obtaining an `IStreamingSubscription` + +`ManagedSession.DefaultStreaming` lazily constructs a shared instance +the first time it is accessed and disposes it when the session is +disposed. + +```csharp +ManagedSession session = await new ManagedSessionBuilder(configuration, telemetry) + .UseEndpoint(endpoint) + .ConnectAsync(ct); + +IStreamingSubscription streaming = session.DefaultStreaming; +``` + +If you need to bind to a specific `ISubscriptionManager` (for example +in tests or with the raw `Session`'s V2 engine), construct directly: + +```csharp +if (!session.TryGetSubscriptionManager(out var manager)) +{ + throw new InvalidOperationException("A V2 subscription engine is required."); +} +await using var streaming = new StreamingSubscription( + subscriptionManager: manager, + subscriptionOptions: new SubscriptionOptions + { + PublishingInterval = TimeSpan.FromMilliseconds(250), + KeepAliveCount = 10, + LifetimeCount = 100, + }); +``` + +The underlying OPC UA subscription is **not** created until the first +`SubscribeXxxAsync` call. Once created it is shared by every active +`SubscribeXxxAsync` enumerator on this `StreamingSubscription`. + +### Subscribing to data changes + +```csharp +await foreach (DataValueChange change in streaming + .SubscribeDataChangesAsync(VariableIds.Server_ServerStatus_CurrentTime, ct: ct) + .ConfigureAwait(false)) +{ + Console.WriteLine($"{change.MonitoredItem?.Name}: {change.Value}"); +} +``` + +For multiple variables in a single stream: + +```csharp +var nodes = new[] +{ + VariableIds.Server_ServerStatus_CurrentTime, + VariableIds.Server_ServerStatus_SecondsTillShutdown, +}; + +await foreach (DataValueChange change in streaming + .SubscribeDataChangesAsync(nodes, options: new MonitoredItemOptions + { + SamplingInterval = TimeSpan.FromMilliseconds(500), + QueueSize = 5, + }, ct: ct).ConfigureAwait(false)) +{ + Console.WriteLine($"{change.MonitoredItem?.ClientHandle}: {change.Value}"); +} +``` + +Each `MonitoredItemOptions` is treated as a template; only `StartNodeId` +is overridden internally per node. + +### Subscribing to events + +```csharp +EventFilter filter = new AlarmEventFilterBuilder() + .ForAlarms() + .Build(); + +await foreach (EventNotification evt in streaming + .SubscribeEventsAsync(ObjectIds.Server, filter, ct: ct) + .ConfigureAwait(false)) +{ + // Raw access; for typed records use SubscribeAlarmsAsync. + Console.WriteLine($"#{evt.Fields.Count} fields"); +} +``` + +`SubscribeEventsAsync` does *not* validate the filter. Use +`AlarmEventFilterBuilder` for Part 9 events and any custom +`SimpleAttributeOperand`-based filter for other event types. + +### Composing streams + +`StreamingSubscriptionExtensions` ships four LINQ-style helpers +designed for the bounded-observation use cases. They wrap the source +`IAsyncEnumerable` and stop iterating (which disposes the +underlying enumerator and the monitored item) when their condition +fires. + +```csharp +// 1) Wait until a predicate matches, then return the matching item. +DataValueChange completed = await streaming + .SubscribeDataChangesAsync(buildProgressVariable) + .TakeUntilAsync(c => (double)c.Value.Value >= 100.0) + .LastAsync(ct); + +// 2) Cap by wall-clock time. Completes silently when the timeout elapses. +List samples = await streaming + .SubscribeDataChangesAsync(sensor) + .WithTimeoutAsync(TimeSpan.FromSeconds(30)) + .ToListAsync(ct); + +// 3) Take exactly N values. +List firstThree = await streaming + .SubscribeDataChangesAsync(sensor) + .TakeAsync(3) + .ToListAsync(ct); + +// 4) Buffer N items into a list — for one-shot snapshots. +IReadOnlyList snapshot = await streaming + .SubscribeDataChangesAsync(sensor) + .BufferedAsync(count: 10, ct); +``` + +`TakeUntilAsync` yields the matching item *last*, so callers can grab +the transition value with `LastAsync` / a terminating handler. + +### Lifecycle, cancellation, disposal + +Each stream has a bounded local buffer, including the default data-change +subscription. Its capacity is `max(1, QueueSize) * monitoredItemCount`, capped +at `int.MaxValue`; event streams default to ten entries per monitored item. +When full, `DiscardOldest` chooses between evicting the oldest buffered entry +and discarding the incoming entry. `StreamingSubscription.DroppedNotificationCount` +reports the cumulative local drops across data-change and event streams. + +For a multi-node stream, this is one shared queue, not a per-node allocation. +With `DiscardOldest` enabled, a busy node can evict a quiet node's only queued +value. There is no local guarantee that each node's latest value remains buffered, +and overflow increments the drop counter rather than failing the stream. Use +separate single-node streams, consumed independently, when per-node retention is +required. Server-side per-monitored-item queues do not change this local policy. + +The streaming subscription guarantees three invariants: + +1. **Lazy subscription creation.** No OPC UA `CreateSubscription` + round-trip happens until your first `SubscribeXxxAsync` call. +2. **Reference-counted monitored items.** Each call adds a monitored + item; disposing the enumerator (end of the `await foreach`, an + exception, or explicit `await using`) removes that monitored item. + The underlying subscription stays alive for other enumerators. +3. **Disposal order.** Disposing the `StreamingSubscription` completes + all open channels and attempts to delete the underlying OPC UA subscription. + Disposing the `ManagedSession` calls this for you. + +V2 subscription disposal gives each server-side delete a separate five-second +cancellation deadline. Local cleanup does not wait indefinitely for an unavailable +server. A failed or timed-out delete is logged; it does not prove remote deletion, +and the server may retain the subscription until its configured lifetime expires. + +Publish workers survive recovery for subscriptions that the V2 engine actually +created. A never-created subscription does not inherit worker retention from a +classic subscription or a V2 subscription that has already been removed. +An intentionally disabled or deleted subscription likewise contributes no +retained demand until it is created again. + +Cancellation propagates the natural way: pass a `CancellationToken` to +`SubscribeXxxAsync` *or* the outer `await foreach` (via +`WithCancellation`). When the token fires the enumerator stops, the +finally block removes the monitored item, and the loop body throws +`OperationCanceledException`. + +```csharp +using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); +try +{ + await foreach (DataValueChange change in + streaming.SubscribeDataChangesAsync(sensor, ct: cts.Token)) + { + Process(change); + } +} +catch (OperationCanceledException) { /* expected */ } +``` + +### Pairing with `AlarmClient` + +The streaming subscription and `AlarmClient` are complementary: + +- `IStreamingSubscription` delivers the *events* (raw or typed). +- `AlarmClient` calls the *methods* (acknowledge, shelve, suppress, + reset, etc.). + +```csharp +AlarmClient alarms = session.GetAlarmClient(); +IStreamingSubscription streaming = session.DefaultStreaming; + +await foreach (ConditionTypeRecord rec in streaming + .SubscribeAlarmsAsync(ObjectIds.Server) + .TakeUntilAsync(r => + r is AlarmConditionTypeRecord a && + a.ConditionId == myAlarmId && + a.ActiveStateId == true)) +{ + if (rec is AlarmConditionTypeRecord active && active.ActiveStateId == true) + { + await alarms.AcknowledgeAsync( + active.ConditionId, + active.EventId, + new LocalizedText("en", "Auto-acked")).ConfigureAwait(false); + } +} +``` + +See [AlarmsAndConditions.md](AlarmsAndConditions.md) for the typed +record hierarchy. + ## Server retransmission queues +On the server, newly created monitored items remain provisional until +the subscription rechecks ownership and deletion after node-manager +creation. If transfer, session detachment, deletion, or cancellation wins, +normal node-manager deletion releases those unattached items. + +Publish, queued Publish, Republish, and persistence consumers receive +deep notification snapshots. Acknowledgement, eviction, or queue disposal +can recycle the retained pooled graph without changing a response still +waiting for encoding or a captured retransmission/durable snapshot. +Pooling remains enabled for the retained notifications. + The server retains notification messages for acknowledgement and Republish (Part 4 §5.14). When a new batch exceeds the configured retransmission capacity, only the excess oldest retained messages are evicted. The remaining sequence @@ -412,7 +657,11 @@ when needed. introspect the partition layout: ```csharp - ISubscription subscription = session.SubscriptionManager.Add(handler, optionsMonitor); + if (!session.TryGetSubscriptionManager(out var manager)) + { + throw new InvalidOperationException("A V2 subscription engine is required."); + } + ISubscription subscription = manager.Add(handler, optionsMonitor); if (subscription is IPartitionedSubscription partitioned) { Console.WriteLine($"Spread over {partitioned.PartitionCount} partition(s)"); @@ -615,242 +864,6 @@ This is the only multi-partition feature gap — single-partition workloads cover the vast majority of OPC UA client use cases unchanged. -## Streaming subscriptions - -`IStreamingSubscription` exposes OPC UA subscription notifications as -`IAsyncEnumerable` streams. Each `SubscribeXxxAsync` call adds a -monitored item to a shared, lazy-created OPC UA subscription and pipes -notifications through a `System.Threading.Channels.Channel`. -Disposing the enumerator removes the monitored item. - -This API sits on top of the V2 subscription engine -(`src/Opc.Ua.Client/Subscription`). It is **not** a replacement -for either the classic or V2 callback-based API — it is a thin -abstraction targeted at three concrete client scenarios: - -- **State-machine waits** — "subscribe, observe transitions until X, - unsubscribe". -- **Short-lived monitoring** — "I need a sample for 30 seconds". -- **Typed alarm streaming** — Part 9 alarm records via - `AlarmStreamExtensions` (see [AlarmsAndConditions.md](AlarmsAndConditions.md)). - -For long-lived application subscriptions, the callback-based -`ISubscriptionManager.Add` + `ISubscriptionNotificationHandler` API -remains the right choice (see -[Sessions.md §4.2 *DefaultSubscriptionEngine (V2)*](Sessions.md#defaultsubscriptionengine-v2)). - -### Obtaining an `IStreamingSubscription` - -`ManagedSession.DefaultStreaming` lazily constructs a shared instance -the first time it is accessed and disposes it when the session is -disposed. - -```csharp -ManagedSession session = await new ManagedSessionBuilder(configuration, telemetry) - .UseEndpoint(endpoint) - .ConnectAsync(ct); - -IStreamingSubscription streaming = session.DefaultStreaming; -``` - -If you need to bind to a specific `ISubscriptionManager` (for example -in tests or with the raw `Session`'s V2 engine), construct directly: - -```csharp -var streaming = new StreamingSubscription( - subscriptionManager: session.SubscriptionManager, - subscriptionOptions: new SubscriptionOptions - { - PublishingInterval = TimeSpan.FromMilliseconds(250), - KeepAliveCount = 10, - LifetimeCount = 100, - }); -``` - -The underlying OPC UA subscription is **not** created until the first -`SubscribeXxxAsync` call. Once created it is shared by every active -`SubscribeXxxAsync` enumerator on this `StreamingSubscription`. - -### Subscribing to data changes - -```csharp -await foreach (DataValueChange change in streaming - .SubscribeDataChangesAsync(VariableIds.Server_ServerStatus_CurrentTime, ct: ct) - .ConfigureAwait(false)) -{ - Console.WriteLine($"{change.MonitoredItem?.Name}: {change.Value}"); -} -``` - -For multiple variables in a single stream: - -```csharp -var nodes = new[] -{ - VariableIds.Server_ServerStatus_CurrentTime, - VariableIds.Server_ServerStatus_SecondsTillShutdown, -}; - -await foreach (DataValueChange change in streaming - .SubscribeDataChangesAsync(nodes, options: new MonitoredItemOptions - { - SamplingInterval = TimeSpan.FromMilliseconds(500), - QueueSize = 5, - }, ct: ct).ConfigureAwait(false)) -{ - Console.WriteLine($"{change.MonitoredItem?.ClientHandle}: {change.Value}"); -} -``` - -Each `MonitoredItemOptions` is treated as a template; only `StartNodeId` -is overridden internally per node. - -### Subscribing to events - -```csharp -EventFilter filter = new AlarmEventFilterBuilder() - .ForAlarms() - .Build(); - -await foreach (EventNotification evt in streaming - .SubscribeEventsAsync(ObjectIds.Server, filter, ct: ct) - .ConfigureAwait(false)) -{ - // Raw access; for typed records use SubscribeAlarmsAsync. - Console.WriteLine($"#{evt.Fields.Count} fields"); -} -``` - -`SubscribeEventsAsync` does *not* validate the filter. Use -`AlarmEventFilterBuilder` for Part 9 events and any custom -`SimpleAttributeOperand`-based filter for other event types. - -### Composing streams - -`StreamingSubscriptionExtensions` ships four LINQ-style helpers -designed for the bounded-observation use cases. They wrap the source -`IAsyncEnumerable` and stop iterating (which disposes the -underlying enumerator and the monitored item) when their condition -fires. - -```csharp -// 1) Wait until a predicate matches, then return the matching item. -DataValueChange completed = await streaming - .SubscribeDataChangesAsync(buildProgressVariable) - .TakeUntilAsync(c => (double)c.Value.Value >= 100.0) - .LastAsync(ct); - -// 2) Cap by wall-clock time. Completes silently when the timeout elapses. -List samples = await streaming - .SubscribeDataChangesAsync(sensor) - .WithTimeoutAsync(TimeSpan.FromSeconds(30)) - .ToListAsync(ct); - -// 3) Take exactly N values. -List firstThree = await streaming - .SubscribeDataChangesAsync(sensor) - .TakeAsync(3) - .ToListAsync(ct); - -// 4) Buffer N items into a list — for one-shot snapshots. -IReadOnlyList snapshot = await streaming - .SubscribeDataChangesAsync(sensor) - .BufferedAsync(count: 10, ct); -``` - -`TakeUntilAsync` yields the matching item *last*, so callers can grab -the transition value with `LastAsync` / a terminating handler. - -### Lifecycle, cancellation, disposal - -Each stream has a bounded local buffer, including the default data-change -subscription. Its capacity is `max(1, QueueSize) * monitoredItemCount`, capped -at `int.MaxValue`; event streams default to ten entries per monitored item. -When full, `DiscardOldest` chooses between evicting the oldest buffered entry -and discarding the incoming entry. `StreamingSubscription.DroppedNotificationCount` -reports the cumulative local drops across data-change and event streams. - -For a multi-node stream, this is one shared queue, not a per-node allocation. -With `DiscardOldest` enabled, a busy node can evict a quiet node's only queued -value. There is no local guarantee that each node's latest value remains buffered, -and overflow increments the drop counter rather than failing the stream. Use -separate single-node streams, consumed independently, when per-node retention is -required. Server-side per-monitored-item queues do not change this local policy. - -The streaming subscription guarantees three invariants: - -1. **Lazy subscription creation.** No OPC UA `CreateSubscription` - round-trip happens until your first `SubscribeXxxAsync` call. -2. **Reference-counted monitored items.** Each call adds a monitored - item; disposing the enumerator (end of the `await foreach`, an - exception, or explicit `await using`) removes that monitored item. - The underlying subscription stays alive for other enumerators. -3. **Disposal order.** Disposing the `StreamingSubscription` completes - all open channels and attempts to delete the underlying OPC UA subscription. - Disposing the `ManagedSession` calls this for you. - -V2 subscription disposal gives each server-side delete a separate five-second -cancellation deadline. Local cleanup does not wait indefinitely for an unavailable -server. A failed or timed-out delete is logged; it does not prove remote deletion, -and the server may retain the subscription until its configured lifetime expires. - -Publish workers survive recovery for subscriptions that the V2 engine actually -created. A never-created subscription does not inherit worker retention from a -classic subscription or a V2 subscription that has already been removed. -An intentionally disabled or deleted subscription likewise contributes no -retained demand until it is created again. - -Cancellation propagates the natural way: pass a `CancellationToken` to -`SubscribeXxxAsync` *or* the outer `await foreach` (via -`WithCancellation`). When the token fires the enumerator stops, the -finally block removes the monitored item, and the loop body throws -`OperationCanceledException`. - -```csharp -using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); -try -{ - await foreach (DataValueChange change in - streaming.SubscribeDataChangesAsync(sensor, ct: cts.Token)) - { - Process(change); - } -} -catch (OperationCanceledException) { /* expected */ } -``` - -### Pairing with `AlarmClient` - -The streaming subscription and `AlarmClient` are complementary: - -- `IStreamingSubscription` delivers the *events* (raw or typed). -- `AlarmClient` calls the *methods* (acknowledge, shelve, suppress, - reset, etc.). - -```csharp -AlarmClient alarms = session.GetAlarmClient(); -IStreamingSubscription streaming = session.DefaultStreaming; - -await foreach (ConditionTypeRecord rec in streaming - .SubscribeAlarmsAsync(ObjectIds.Server) - .TakeUntilAsync(r => - r is AlarmConditionTypeRecord a && - a.ConditionId == myAlarmId && - a.ActiveStateId == true)) -{ - if (rec is AlarmConditionTypeRecord active && active.ActiveStateId == true) - { - await alarms.AcknowledgeAsync( - active.ConditionId, - active.EventId, - new LocalizedText("en", "Auto-acked")).ConfigureAwait(false); - } -} -``` - -See [AlarmsAndConditions.md](AlarmsAndConditions.md) for the typed -record hierarchy. - ## Classic → V2 surface mapping This section is the working reference for porting classic diff --git a/docs/TransferSubscription.md b/docs/TransferSubscription.md index ee67a788a3..235ca3f851 100644 --- a/docs/TransferSubscription.md +++ b/docs/TransferSubscription.md @@ -1,50 +1,62 @@ # TransferSubscriptions +## Contents + +- [Overview](#overview) +- [Porting an existing server](#porting-an-existing-server) +- [Known limitations and issues](#known-limitations-and-issues) +- [Recovering from unsolicited `Good_SubscriptionTransferred`](#recovering-from-unsolicited-good_subscriptiontransferred) + - [Caveats](#caveats) + ## Overview -The transfer subscription service is fundamental for *zero data loss* scenarios, where network connections are interrupted, clients have to restart or load balancing requires to transfer sessions to other clients. -Using the transfer subscription service set allows to recover from such situations and to keep the data loss as minimal as possible. +`TransferSubscriptions` moves existing server subscriptions to another session. +It can reduce data loss during reconnect or client restart by preserving the +server's monitored items and queued notifications. It does not guarantee zero +loss: subscription lifetime, queue capacity, retransmission availability, and +authorization still apply. -The TransferSubscriptions service for the Server and Client libraries is supported from Nuget version `1.4.368` and consists of the following elements: +The server and client implement the +[Part 4 TransferSubscriptions service](https://reference.opcfoundation.org/Core/Part4/v105/docs/5.14.7). +The target session must have permission to transfer the subscriptions. Transfer +also relies on the server's `GetMonitoredItems` method when the client needs to +recover monitored-item handles. -* Updated C# Stack that supports the *TransferSubscriptions* service for Client and Server as specified in in [Part 4](https://reference.opcfoundation.org/v104/Core/docs/Part4/5.13.7/) -* The updated server library supports to - * Transfer subscriptions with optional inital values according to the service specification using the service call. - * **Breaking change**: Modified `INodeManager` interface which is extended with a `TransferMonitoredItems` [method](https://github.com/OPCFoundation/UA-.NETStandard/blob/8c1a659ecf5c1616b3b7f132706324b90f9ff015/src/Opc.Ua.Server/NodeManager/INodeManager.cs#L309), to support the transfer of the monitored items hosted in various types of NodeManagers and to implement a unified way to queue initial values. - * Ported sample `NodeManager` implementations, which can be taken as a reference to port other custom implementations, e.g. [here](https://github.com/OPCFoundation/UA-.NETStandard/blob/8c1a659ecf5c1616b3b7f132706324b90f9ff015/samples/Quickstarts.Servers/SampleNodeManager/SampleNodeManager.cs#L2937). -* Updated client library which supports multiple ways to transfer subscriptions, applicable for any compliant server. -The following use cases are supported to transfer a subscription: - * **Transfer from active session**: The client creates a new session in the same process and transfers subscriptions into the new session while the old session is still active. - * **Transfer from closed session**: The client closes the old session but has the new `DeleteSubscriptionsOnClose` property set to `false`. The old session is closed, but the subscriptions remain *abandoned* on the server and keep collecting samples. The client creates a new session and transfers the abandoned subscriptions to the new session, for which the client library has still all information available. - * **Client restart from persisted storage**: The client saves subscriptions to a persisted xml stream, restarts itself, creates a new session, loads the saved subscriptions from xml stream and transfers the subscriptions on the server. - * **Transfer after session reconnect**: The client `SessionReconnectHandler` can not reconnect to the existing session, it creates a new session and tries to transfer a subscription before recreating the full subscriptions. -* Client Session library enhancements: - * A new Session property `DeleteSubscriptionsOnClose` which, if set to `false`, does not delete the subscriptions if a session is closed on the server. To preserve the legacy behavior, the default of the property is `true`. - * A new Subscription property `RepublishAfterRestart` which assumes, if set to `true`, that unacknowledged publish requests on the server after a transfer need to be republished to minimize the data loss. Otherwise remaining publish requests are only acknowledged after the transfer. - * A new `Session.TransferSubscriptions` [method](https://github.com/OPCFoundation/UA-.NETStandard/blob/8c1a659ecf5c1616b3b7f132706324b90f9ff015/src/Opc.Ua.Client/Session.cs#L3410) to transfer a subscription managed by the client library. The new API can implicitly handle all of the above mentioned use cases. A requirement on the server side is not only the support of the TransferSubscriptions service, but also the implementation of the `GetMonitoredItems` ([see Part5](https://reference.opcfoundation.org/v104/Core/docs/Part5/9.1/)) standard method. - * Existing clients which use the `SessionReconnectHandler` get the improved support (no client changes necessary). -* The updated C# [Reference Server](../samples/Reference/ConsoleReferenceServer) with ported NodeManager samples to support the new [TransferSubscriptions](https://reference.opcfoundation.org/v104/Core/docs/Part4/5.13.7/) service set. -* Multiple [unit tests](https://github.com/OPCFoundation/UA-.NETStandard/blob/8c1a659ecf5c1616b3b7f132706324b90f9ff015/tests/Opc.Ua.Client.Tests/SubscriptionTest.cs#L455) demonstrating the use cases for subscription transfer. -* Updated NodeManagers with reference implementation of the necessary functions. +For current client APIs: + +- Use [ManagedSession recovery](Sessions.md#reconnect-semantics-on-managedsession) + for connection recovery; do not add `SessionReconnectHandler` around a + `ManagedSession`. +- Use [subscription persistence and transfer](Subscriptions.md#persistence-save--load) + to save client state and restore it after restart. A transfer attempts to + recover an existing server subscription; recreating one starts new queues. +- If subscriptions must survive a deliberate session close, configure + `DeleteSubscriptionsOnClose = false` before closing it. They remain subject to + their server-side lifetime; this does not make them durable across server restart. + See [durable subscriptions](DurableSubscription.md) for that separate capability. ## Porting an existing server -If a server is derived from the StandardServer class and if the custom NodeManagers all support the new TransferMonitoredItems method, the support becomes implicitly available. -Typically the following porting steps are necessary: +Servers based on `StandardServer` and the built-in node-manager bases already +participate in monitored-item transfer. A custom manager implementing the +interfaces directly must implement the transfer contract, including initial-value +queuing when requested, without retaining stale source-session ownership. +See [node managers](NodeManagers.md) and the +[2.0 session/subscription migration guide](migrate/2.0.x/sessions-subscriptions.md). -* Use the new `MonitoredItem` constructor which has no `Session` parameter, it is implicitly available in the `Subscription` and the `MonitoredItem`can not keep a private reference when the subscription is transferred. -* Add the `TransferMonitoredItems` method from another `NodeManager` sample to the custom `NodeManager` implementations. -* Depending on the `NodeManager` implementation, add or fix the `ReadInitialValue` method. The monitored item transfer must be able to queue an unfiltered initial value, if requested. -* More subtle changes might be required, e.g. how the monitored item handle to read the attributes is obtained. -* Once the server builds, if available run a CTT test against a node in the ported NodeManager. -* A sample Commit which ports the NetStandard-Samples codebase from 367 to 368 is [here](https://github.com/OPCFoundation/UA-.NETStandard-Samples/pull/267/commits/5d990b7f39880941a5e788d17b903fd41254a804). +Transfer support was introduced in 1.4.368. The +[historical sample port](https://github.com/OPCFoundation/UA-.NETStandard-Samples/pull/267/commits/5d990b7f39880941a5e788d17b903fd41254a804) +is relevant only when upgrading from releases predating that support; it is not +the current SDK API reference. ## Known limitations and issues -* **There is no opt out**. -* **Breaking change**: There is currently no support for NodeManagers to *not* support the new transfer service. Unless the NodeManagers are all ported to support the monitored items transfer, build errors will prevent from using the latest 1.4.368 library. -* There is no client sample for special use cases like e.g. the client restart in a docker container. -* In some .NET Core 3.1 projects a warning CS8032 occurs due to missing analyzer. Current believe is this warning can be safely disabled. +- Transfer cannot recover samples already lost to queue overflow or expired + subscriptions. +- Server restart requires server-side durability or mirroring to retain the + subscription; saving client configuration alone is insufficient. +- Recovery from an unsolicited transfer notification is an explicit compatibility + policy, described below, not a substitute for legitimate cross-session transfer. ## Recovering from unsolicited `Good_SubscriptionTransferred` diff --git a/docs/Transports.md b/docs/Transports.md index 461facfc86..639f3b59cf 100644 --- a/docs/Transports.md +++ b/docs/Transports.md @@ -7,6 +7,17 @@ to host or connect to OPC UA servers over something other than the default `opc.tcp://` transport — for example to traverse firewalls or to integrate with web-based tooling. +## Contents + +- [Transport profile matrix](#transport-profile-matrix) +- [Assembly layout](#assembly-layout) +- [Server-side configuration](#server-side-configuration) +- [Client-side usage](#client-side-usage) +- [Discovery](#discovery) +- [Opt-in: Kestrel-hosted `opc.tcp`](#opt-in-kestrel-hosted-opctcp) +- [Implementing a custom byte transport](#implementing-a-custom-byte-transport) +- [See also](#see-also) + ## Transport profile matrix | Profile | URL scheme | Wire format | UA Secure Conversation | Security modes | @@ -499,7 +510,7 @@ wire. token while awaiting peer data — long-lived sessions rely on this to tear down cleanly. - [ ] Error mapping. Map transport-layer errors to - [`ServiceResultException`](../src/Opc.Ua.Core/Types/Result/ServiceResultException.cs) + [`ServiceResultException`](../src/Opc.Ua.Types/Utils/ServiceResultException.cs) with the matching `StatusCodes.BadXxx` (`BadConnectionClosed`, `BadTcpMessageTypeInvalid`, `BadTcpMessageTooLarge`, …) so the channel can route them through normal UA fault paths. diff --git a/docs/Vision.md b/docs/Vision.md index e1cec33b48..062c1370eb 100644 --- a/docs/Vision.md +++ b/docs/Vision.md @@ -7,6 +7,47 @@ companion specification, plus the OpenUSD offscreen capture adapter and the Model Context Protocol tool package that lets a language-model agent see through a Vision server and act on what it sees. +## Contents + +- [Packages](#packages) +- [Two perception paths behind one contract](#two-perception-paths-behind-one-contract) +- [Minimal hosted server](#minimal-hosted-server) +- [Hosting API](#hosting-api) + - [`VisionServerOptions`](#visionserveroptions) +- [Build context](#build-context) + - [Without DI](#without-di) +- [Topology builders](#topology-builders) + - [Frames](#frames) + - [Sensors](#sensors) + - [Pipelines](#pipelines) + - [Providers](#providers) +- [§5.12 conventions](#512-conventions) +- [§6.4 media gating](#64-media-gating) +- [Rendering without pixels](#rendering-without-pixels) +- [Facets supported](#facets-supported) +- [Using the client libraries](#using-the-client-libraries) + - [Registration](#registration) + - [Discovery](#discovery) + - [Reading a detection](#reading-a-detection) + - [Composing a pose](#composing-a-pose) + - [Submitting feedback](#submitting-feedback) + - [Streaming detections](#streaming-detections) +- [MCP tools](#mcp-tools) +- [Sample: bin-picking](#sample-bin-picking) + - [Scene lighting](#scene-lighting) + - [Feedback validation](#feedback-validation) +- [Limitations](#limitations) +- [Visual inspection: a cross-companion cell](#visual-inspection-a-cross-companion-cell) + - [The model never decides](#the-model-never-decides) + - [Address-space composition](#address-space-composition) + - [Recipe and verdict rule](#recipe-and-verdict-rule) + - [Why uncertainty is physical](#why-uncertainty-is-physical) + - [Inspection loop](#inspection-loop) + - [Escalation and ground truth](#escalation-and-ground-truth) + - [Modes](#modes) + - [What is deliberately not implemented](#what-is-deliberately-not-implemented) +- [See also](#see-also) + > **Draft.** The namespace `http://opcfoundation.org/UA/Vision/` and every > NodeId in it are provisional. The API is stable within this repository but > every ObjectType, DataType and BrowseName can still change when the @@ -752,7 +793,6 @@ characteristic. > training label, and a false positive is corrected by asserting that > nothing replaces it. Neither statement can be made by submitting an > array, because both *are* the empty array. -> > The pairing is checked in both directions. An empty `Detections` > without `SceneIsEmpty` is refused — the flag is what distinguishes a > deliberate observation from a lost payload — and `SceneIsEmpty` with @@ -892,19 +932,15 @@ on: - **Zero-norm quaternion or pose with fewer than three position components** — refused with the detection index. -An **empty** detection set is refused too, with `Bad_InvalidArgument`, -because §9.5 states it plainly: "`Detections` empty" is an argument -error. So is a `SubmitCorrection` whose corrected arrays are both empty -or both populated — §9.5 requires *exactly one* to be non-empty. +An empty detection set is accepted only when `SceneIsEmpty` is set; setting +that flag with a nonempty set is rejected. For corrections, `RetractAll` allows +both corrected arrays to be empty. Otherwise exactly one corrected array must +be nonempty, and `RetractAll` must not accompany corrected entries. -That is worth dwelling on, because it means two useful statements cannot -be made at all. An agent that has emptied the bin cannot report "I looked -and there is nothing there"; it must either invent a detection or say -nothing. And a false positive — the model saw something that was not -there — cannot be retracted by correcting the result down to an empty -set, which is one of the more valuable labels a correction could carry. -The implementation conforms rather than deviating, and the gap is raised -against the draft; see [Limitations](#limitations). +These flags let an agent report an empty scene or retract a false positive +without inventing a detection. See [Submitting feedback](#submitting-feedback) +for the payload contract and [Limitations](#limitations) for the host's +responsibility for learning counters. ## Limitations diff --git a/docs/WebApi.md b/docs/WebApi.md index 6006005f6b..718ccb3c06 100644 --- a/docs/WebApi.md +++ b/docs/WebApi.md @@ -10,6 +10,20 @@ The binding ships as part of mounted on the same Kestrel host as the binary and `application/opcua+uajson` sub-profiles). +## Contents + +- [Quick start](#quick-start) +- [Routes (full coverage)](#routes-full-coverage) +- [Encoding negotiation](#encoding-negotiation) +- [Discovery](#discovery) +- [Wire format](#wire-format) +- [Authentication](#authentication) + - [JWT claim projection (built-in)](#jwt-claim-projection-built-in) +- [Hosting modes](#hosting-modes) +- [Long-poll `/publish`](#long-poll-publish) +- [Client integration](#client-integration) +- [Related plans and follow-ups](#related-plans-and-follow-ups) + - **Server side**: ASP.NET Core Minimal-API endpoints (one `MapPost` per spec service) — **NativeAOT-compatible**; no MVC reflection, no `[UnconditionalSuppressMessage]` attributes. @@ -121,11 +135,11 @@ endpoint description with all of these fields pre-populated. > `WebApiWssTransportChannel` on the client side, > `HttpsTransportListener.AcceptWebSocketOpenApiAsync` on the server > side. Fluent shortcut: -> `ManagedSessionBuilder.UseWssOpenApiEndpoint(url, encoding)`. Bearer -> tokens ride in the sub-protocol name +> `ManagedSessionBuilder.UseWssOpenApiEndpoint(url, encoding)`. The bearer +> token is encoded in the selected sub-protocol name > (`opcua+openapi+`) because browser WebSocket APIs forbid > custom HTTP request headers. -> + > **Security considerations for the bearer-prefix sub-protocol:** > the WebSocket spec requires the server to echo the selected > sub-protocol back to the client in the 101 handshake. The bearer @@ -135,7 +149,7 @@ endpoint description with all of these fields pre-populated. > request is not `IsHttps`). > 2. The server's Kestrel access log (`Sec-WebSocket-Protocol` header). > 3. Any HTTP proxy / WAF / load-balancer log on the path. -> + > Operators **must** redact the `Sec-WebSocket-Protocol` header from > logs and **should** use short-lived tokens (≤ 60 s TTL) so a > captured token expires before it can be replayed. The server also diff --git a/docs/WhatsNewIn2.0.md b/docs/WhatsNewIn2.0.md index 8a844f67e6..7496d9f907 100644 --- a/docs/WhatsNewIn2.0.md +++ b/docs/WhatsNewIn2.0.md @@ -1,574 +1,205 @@ # What's New in OPC UA .NET Standard 2.0 -This document is a developer-facing tour of the changes between **1.5.378** and -**2.0**. It is organised by theme and layer; each section describes the -broad-stroke change with a paragraph and links to the deeper feature -documentation in this folder. +Version 2.0 expands the SDK beyond the 1.5.378 client/server APIs with managed +connections, model-driven application development, injectable services, and +distributed deployment options. This overview highlights what those changes +enable; the linked guides contain API contracts, examples, and limitations. -If you are migrating an existing application, the companion -[Migration Guide](MigrationGuide.md) is the prescriptive, API-level reference. +## Contents + +- [At a glance](#at-a-glance) +- [Feature details](#feature-details) + - [Dependency injection and hosting](#dependency-injection-and-hosting) + - [Client](#client) + - [Source generators and modeling](#source-generators-and-modeling) + - [Type system and immutability](#type-system-and-immutability) + - [Performance, memory, and pooling](#performance-memory-and-pooling) + - [Async, cancellation, and `TimeProvider`](#async-cancellation-and-timeprovider) + - [Native AOT](#native-aot) + - [OPC UA companion-spec coverage](#opc-ua-companion-spec-coverage) + - [Security and certificates](#security-and-certificates) + - [Global Discovery Server](#global-discovery-server) + - [Part 14 PubSub modernization](#part-14-pubsub-modernization) + - [High availability and redundancy](#high-availability-and-redundancy) + - [Tooling and diagnostics](#tooling-and-diagnostics) +- [Further reading](#further-reading) ## At a glance -- **The OPC UA built-in types are now allocation-friendly value types**, with - a `Variant`-first public API, a new `ByteString` type, and `ArrayOf` / - `MatrixOf` replacing untyped array shapes. -- **Server- and client-side stacks are now fully `async`/`await`** with - cancellation flowing through the request lifetime, `TimeProvider` everywhere, - and the new `AsyncCustomNodeManager` powering all built-in NodeManagers. -- **A first-class hosting story**: `services.AddOpcUa()` and `IOpcUaBuilder` - plug servers and clients into `Microsoft.Extensions.DependencyInjection` and - the .NET Generic Host, complemented by a fluent server and `ManagedSession` - fluent client builder. -- **Native AOT support across the stack**, including AOT-clean source - generators, NodeSet export/import, and reference servers/clients. -- **New companion-spec coverage**: Part 9 (Alarms & Conditions), Part 11 - (Historical Access) + Part 13 (Aggregates), Part 16 (State Machines), - Part 17 (Alias Names), Part 18 (Role Management), Part 20 (File Transfer), - Part 100 (Device Integration), OPC 10100-1 WoT Connectivity, and a Local - Discovery Server. -- **Source generators emit NodeManagers, typed `ObjectType` proxies, and - `IEncodeable` data types from model design XML**, removing hand-written - boilerplate while staying AOT-clean. -- **GDS is now Part 12 full-compliance**, with arbitrary certificate groups, - custom group support, modernised Push/Pull APIs, and OPC 10000-21 - registrar ticket administration backed by the generated Onboarding model. -- **OPC UA Part 4 §6.6 redundancy plus opt-in distributed high availability.** - Server and client redundancy (`Server.ServerRedundancy` / `ServiceLevel`, - `ManagedSession.WithServerRedundancy()`) ship in the box, and new - `Opc.Ua.Redundancy(.Server/.Client/.Kubernetes)` packages add distributed - address-space / session / subscription mirroring over an in-package CRDT - (eventual) or Raft (strong) shared store, with a Kubernetes deployment guide. -- **An MCP server** ships in the box so an LLM/Copilot can drive an OPC UA - client; and a **2.0 Migration Analyzer + code fixer** automates the - mechanical parts of upgrading to v2. +| Area | What 2.0 enables | +| --- | --- | +| Application development | Host clients and servers through one dependency-injection builder; generate model types and typed client proxies instead of writing service plumbing. | +| Client connections | Use `ManagedSession` for reconnection and failover, with callback-based or asynchronous streaming subscriptions. | +| Data and deployment | Use strongly typed values, arrays, and matrices; publish supported applications with Native AOT. | +| Server capabilities | Add historian providers, alarms, state machines, file access, and the models in the [specification catalogue](#opc-ua-companion-spec-coverage) through reusable services. | +| Security and discovery | Manage certificate lifecycles and identities through providers, and integrate GDS pull/push management and token services. | +| PubSub and availability | Compose publishers/subscribers with transport packages, or opt into redundancy and distributed state for client/server deployments. | -## Breaking changes at a glance - -2.0 is the first major break of the public API since the project moved to -.NET Standard. The biggest sources of breakage are the readonly-struct -built-ins (`NodeId`, `ExtensionObject`, `Variant`, `DataValue`, -`QualifiedName`, `LocalizedText`, `ArrayOf`), the `Variant`-for-`object` -pivot in code and API, the new `IEncodeableFactoryBuilder` and IType -hierarchy, the removal of Newtonsoft.Json from `Opc.Ua.Core`, the -`ManagedSession` family next to the classic `Session`, and the move to -`AsyncCustomNodeManager` for server extensibility. The -[Migration Guide](MigrationGuide.md) walks every break in detail, with the -companion [2.0 Migration Analyzer](#tooling) handling most of the mechanical -edits automatically. - -**ECC security policies now require .NET 8 or later stack assets.** -The .NET Framework and .NET Standard 2.1 builds no longer advertise or accept -ECC SecureChannel or user-token policies: their ECDH API hashes the shared -secret instead of exposing the raw secret required by OPC UA Part 6 HKDF. -Upgrade to the .NET 8+ build, or configure a supported RSA policy on both peers. -Running the .NET Standard build on a newer runtime does not enable ECC policies. -See [ECC platform requirements](EccProfiles.md#known-limitations). - -## Cross-cutting themes - -### Type system and immutability - -The built-in OPC UA types have been redesigned around immutability and -allocation-light value semantics. `NodeId`, `ExpandedNodeId`, -`QualifiedName`, `LocalizedText`, `Variant`, `ExtensionObject`, -`DataValue`, and `ArrayOf` are now `readonly struct`s; `null` checks are -replaced with `IsNull` / `.Null` to align with `INullable`. A new -`ByteString` is preferred over `byte[]` in public API, and `ArrayOf` / -`MatrixOf` provide first-class array and matrix abstractions. The -`object`-typed public surface has been replaced with `Variant` across the -stack and in source-generated code, eliminating boxing in encoders, decoders, -and node-state read/write paths. Equality on `ExtensionObject`, -`StatusCode`, and numeric ranges has been tightened, and `DateTimeUtc` -makes timestamp intent explicit. See the -[2.0 migration guide — Improved Type Safety](migrate/2.0.x/types.md) -section for the per-type deltas. - -### Async, cancellation, and `TimeProvider` - -The server now runs fully on the Task-based asynchronous pattern. The new -[`AsyncCustomNodeManager`](AsyncServerSupport.md) is the canonical base -class for NodeManagers and threads `CancellationToken` through every -service call; built-in managers (`CoreNodeManager`, `DiagnosticsNodeManager`, -`ReferenceNodeManager`) have all migrated to it, and `MonitoredNode` is -`IAsyncNodeManager`-aware. A new server-side `RequestLifetime` propagates -the request-scoped cancellation token through the call stack, so a -client-cancelled request short-circuits cleanly. Synchronous-over-async -patterns have been removed from non-obsolete public APIs. The stack also -adopts `System.TimeProvider` for all timing primitives, replacing direct -`DateTime.UtcNow` and `Timer` use; this is what makes server logic -deterministic under test and tolerant of system-clock changes. +## Feature details ### Dependency injection and hosting -The stack now offers a unified -[`Microsoft.Extensions.DependencyInjection`](DependencyInjection.md) surface: -a single `services.AddOpcUa()` returns an `IOpcUaBuilder`, and every feature -library hangs its own `.AddXxx(...)` extension off it. Servers run as -`IHostedService`s under the .NET Generic Host; options bind from -`Action` or `IConfiguration`; identity providers, certificate manager, -secret store, file system, historian, alarms, and the GDS extensions all -register through the same builder. Alongside DI, a -[source-generated fluent server API](NodeManagers.md#source-generated-node-managers) lets -applications stand up a server from a model design XML with a few -`.AddXxx().WithYyy()` calls; the -[`ManagedSession`](Sessions.md#3-managedsession--the-connection-state-machine-facade) -fluent builder is the equivalent on the client. - -### Native AOT - -The full stack — Core, Types, Client, Server, ComplexTypes, GDS, PubSub, -and the source generators — is now [Native AOT](NativeAoT.md) friendly. -Public API avoids reflection paths that require trimming suppression, the -source generators emit AOT-clean code, and the reference servers/clients -can be published as self-contained single-file native binaries. UANodeSet -import/export and the encodeable factory have been reworked to function -under AOT, and the test matrix includes AOT smoke tests for each shipped -library. - -### Source generators and modeling - -The new source-generation pipeline emits the typical OPC UA boilerplate -from model design XML rather than hand-written code. The -[NodeManager generator](NodeManagers.md#source-generated-node-managers) produces a fully -async, fluent NodeManager skeleton plus typed `*State` properties for every -node; the [DataType generator](SourceGeneratedDataTypes.md) emits -`IEncodeable` implementations from POCO classes; and a new generator emits -**typed method proxies on `ObjectType`**s so callers invoke methods with a -strongly-typed signature rather than a generic `Call` plus variant arrays. -The encodeable factory build path uses an `IEncodeableFactoryBuilder` so -factories can be assembled deterministically and AOT-cleanly; OPC UA -`OptionSet` data types are now backed by generated structures, and -cross-assembly model references are tracked via the -[`ModelDependencyAttribute`](ModelDependencies.md). The generator can also -default a model's instance modelling rules to its type-definition rules, -which the stack now opts into. For Optional `Variable`/`Method` children -the generator emits **five chainable `Add{Child}` overloads** per typed -state class (idempotent ensure-child, `Action` configure, -conditional configure, `Func` replace, conditional -replace) so opt-in extensions of singleton instances collapse into a -single fluent chain. - -### OPC UA companion-spec coverage - -This release substantially extends companion-spec coverage with full -server- and client-side implementations: - -- **Part 9 — Alarms and Conditions**: full server + client implementation - with latched / silenced / out-of-service variants, alarm groups and a - suppression engine, rate metrics, a typed `AlarmClient`, the - `AlarmEventFilterBuilder`, and `IAsyncEnumerable` alarm streaming. See - [Alarms and Conditions](AlarmsAndConditions.md). -- **Part 11 — Historical Access** + **Part 13 — Aggregates**: - provider-based [historical access](HistoricalAccess.md) with standard - [aggregates](Aggregates.md). -- **Part 16 — State Machines**: a unified fluent - [`StateMachineBuilder`](StateMachines.md) with both *definition* - (`FluentFiniteStateMachineState`) and *lifecycle* (attach behaviour to - stack-shipped or generator-emitted FSMs) modes, plus client-side - streaming / read helpers on the generated `*TypeClient` proxies. - Definition-mode machines materialize a `StateType` node per declared - state and a `TransitionType` node per declared transition (with - `StateNumber` / `TransitionNumber`, `FromState` / `ToState` / - `HasEffect` / `HasCause`, and the `AvailableStates` / - `AvailableTransitions` / `LastTransition` children), so - `CurrentState/Id` and `LastTransition/Id` resolve to real nodes and - `HasSubStateMachine` hangs off the parent **state** node per Part 16 - §4.4.16 rather than off the machine root. Callers that passed a state - machine's `ObjectId` to `GetSubStateMachineAsync` must pass a state - NodeId instead — from `GetAvailableStatesAsync` or a snapshot's - `CurrentStateId` — and code that read a numeric id out of - `CurrentState/Id` should call `FiniteStateMachineState.GetStateId` - instead of parsing the identifier. Generated state-machine classes - now also override `ElementNamespaceUri` with the namespace of the - model that declares their states, so `CurrentState/Id` and - `LastTransition/Id` are qualified with the companion-spec namespace - rather than the OPC UA one. -- **Part 17 — Alias Names**: full server + client support for - `AliasNameType`, `AliasNameCategoryType`, `FindAlias`, `FindAliasVerbose`, - `AddAliasesToCategory`, `DeleteAliasesFromCategory`, and `LastChange`. - See [Alias Names](AliasNames.md). -- **Part 18 — Role Management**: full server-side role administration - surface, with the server automatically assigning the OPC UA Part 3 §4.9 - `TrustedApplication` role, and a pluggable - [identity-provider model](IdentityProviders.md) that supports anonymous, - username, X.509, and token-issuer flows (OAuth2 / OIDC / Entra / JWT). -- **Part 20 — File Transfer**: a server-side FileSystem library, with a - matching System.IO-style [`FileSystemClient`](FileSystemClient.md) on - the client. -- **Part 100 — Device Integration**: the `Opc.Ua.Di` / `Opc.Ua.Di.Server` / - `Opc.Ua.Di.Client` library trio with a fluent `IDeviceBuilder`, device - sub-type extensions, lock service, software-update package store, and - client helpers. See [Device Integration](DeviceIntegration.md) and - [Software Update](SoftwareUpdate.md). -- **Parts 210 and 211 — Relative Spatial Location and Global Positioning**: - source-generated released RSL/GPOS models, standalone and composed server - hosting, technology-neutral position providers, typed clients and streams, - frame-chain resolution, WGS84/ENU conversion, and rigid/similarity/affine - Zone fitting. The robot/OpenUSD sample publishes independently configurable - mobile robot poses. See [Positioning](Positioning.md). -- **OPC 40010-1 — Robotics** (over **OPC 40001-1 — Industrial Automation**): - the `Opc.Ua.Robotics` / `Opc.Ua.Robotics.Server` / `Opc.Ua.Robotics.Client` - library trio over Device Integration, with source-generated Robotics 1.02 - and IA models, `AddRobotics` / `ConfigureRobotics` hosting, ordered model - providers, and validated fluent topology builders that assemble motion - device systems, controllers, motion devices, axes, power trains, motors, - gears, drives, safety states, and task controls with the correct - companion-spec references. See [Robotics](Robotics.md), including the draft - Robot Intent task-level command model. -- **OPC UA — Vision** (draft): the `Opc.Ua.Vision` / - `Opc.Ua.Vision.Server` / `Opc.Ua.Vision.Client` / - `Opc.Ua.Vision.OpenUsd` package family, with a source-generated Vision model, - `AddVision` / `ConfigureVision` hosting, media/inference/feedback providers, - fluent frame/sensor/calibration/pipeline builders, typed `VisionClient` - discovery, result streaming, off-server feedback, facet derivation, and - OpenUSD camera capture. See [Vision](Vision.md), including the Robotics + - Vision bin-picking example. -- **OPC UA — AI Model Management and Inference** (draft): the `Opc.Ua.AI` / - `Opc.Ua.AI.Inference` / `Opc.Ua.AI.Server` / `Opc.Ua.AI.Client` package - family over xRegistry, with a source-generated catalogue/deployment/inference - model, the `IInferenceBackend` contract, `Microsoft.Extensions.AI` - `IChatClient` and OpenAI-compatible REST backends, `AINodeManagerFactory` - hosting through `AddNodeManager`, `Invoke` routing, - learning jobs, and standard file-transfer artefact streaming. See - [AI Model Management](AI.md). -- **OPC 10100-1 — WoT Connectivity**: model, server, and client libraries - for surfacing OPC UA servers as Web of Things Thing Descriptions, with - the `WoTAssetConnectionManagement` server methods gated by a - configurable `WotManagementAccessPolicy` (defaults: `SignAndEncrypt` - channel + `SecurityAdmin` role + no anonymous). See - [WoT Connectivity](WoTConnectivity.md). -- **Local Discovery Server**: a built-in LDS implementation usable - standalone or as part of a hosted server. - -Other server-side feature work: -[NodeManagement service set](NodeManagement.md) (`AddNodes`, -`DeleteNodes`, `AddReferences`, `DeleteReferences` with an -`INodeManagementAsyncNodeManager` opt-in and a per-manager -`AllowNodeManagement` gate), -[Model Change Tracking](ModelChangeTracking.md) (server-side -`ModelChangeAggregator` and auto-emitted `GeneralModelChangeEvent`s, with -client-side per-node `INodeCache.InvalidateNode`), and the -[Subscriptions and Monitored Items](Subscriptions.md) service set -(V2 subscription engine, declarative + imperative `SetTriggering` with -N:M support, and `IAsyncEnumerable`-based streaming subscriptions for -state-machine waits and short-lived monitoring). - -### Performance, memory, and pooling - -The type-system rework eliminates a large class of allocations: every -encode/decode of `NodeId`, `Variant`, `DataValue`, `ExtensionObject`, and -their collections now stays on the stack or in pooled buffers. A new -`IPooledEncodeable` activator pool further reduces GC pressure on hot -encode paths. Server-side, `MonitoredNode2` caches role-permission -validation event-driven (no more per-publish recomputation), publishing -queues use channel-based consumers, and `NodeState.ReadAttributes` has -been optimised. Lifetime bugs that surfaced under load were fixed in -several places: socket and event-handler leaks during server restart, -timer leaks in `ChannelAsyncOperation.EndAsync`, `TcpTransportListener` -resource leakage in `ServerBase.StopAsync`, undeleted subscription -diagnostic nodes, and the abandoned-subscription map migrated from a -locked `List` to a `ConcurrentDictionary`. - -### Security and certificates - -A new ref-counted [`Certificate`](Certificates.md) wrapper and the -`CertificateManager` segregated-interface design replace the older -`X509Certificate2` exposure: certificates are tracked deterministically, -shared safely, and disposed predictably across stores, channels, and -identity flows. Secure-channel negotiation has been hardened; the client -now auto-detects and force-renegotiates on server certificate rotation, -and application-certificate lookup can fall back from a concrete -`ApplicationCertificateType` to the abstract type when no concrete entry -matches. The `EncryptedSecret` machinery now supports both RSA and ECC and -is bridge-compatible with legacy .NET / OPC UA implementations. The server -ships with [client lockout](RoleBasedUserManagement.md) for failed -authentication attempts, and `SubCA` revocation no longer auto-creates an -empty CRL on the issuing CA. - -Cryptography is now pluggable. A [`CryptoProvider`](CryptoProvider.md) model -lets an application route cryptographic operations to another library, an -offboard service or hardware, chosen per purpose and per security policy and -injected through `AddCryptoProvider(…)`. The default is unchanged and costs -nothing: everything resolves to platform cryptography until something is bound. -Private keys no longer have to be owned by the certificate — a key can be held -detached, in a TPM, a smart card or an HSM, and never enter process memory. -`Certificate.CopyWithDetachedPrivateKey` is the seam that makes this work on -every platform, which `X509Certificate2.CopyWithPrivateKey` does not. -The optional `OPCFoundation.NetStandard.Opc.Ua.Security.Pkcs11` package adds a -PKCS#11 certificate store addressed by RFC 7512 `pkcs11:` URIs. Which -cryptographic module performed an operation, and whether it carries any -validation, is auditable through logs, metrics and the address space, and can -be constrained with a compliance policy. -The symmetric primitives are pluggable too. `ISymmetricCryptoProvider`, -`IKeyDerivationProvider` and `ISecureRandomSource` let a validated cryptographic -module perform *every* operation rather than only the asymmetric ones, which is -what a FIPS deployment needs. They are optional facets a provider opts into, so -existing providers are unaffected, and they sit behind a null fast path: a -deployment that registers nothing keeps the inline platform code, with no -interface dispatch on the per-message path. A provider bound to a symmetric -purpose it cannot actually perform is reported rather than silently replaced by -the platform, and under `FipsOnly` it refuses to start. -PubSub uses the same seam: the per-message AES-CTR and HMAC a publisher applies -route through a registered provider, so a validated module performs those too. -Full device custody is not achievable for PubSub, and the reason is worth stating -— a standard Security Key Service returns raw key bytes over the wire, so the key -is in process memory by construction. What is bounded instead is its lifetime. -A key served over a network no longer has to occupy a thread. `RSA` and `ECDsa` -are synchronous contracts belonging to .NET, so an implementation opts in by -also implementing `IAsyncRsaKey` or `IAsyncEcdsaKey`, which the stack finds by -type test and uses where it can — user identity token signing and decryption, -session activation, and the secure channel open and renew path. A software key -implements neither, so those paths complete synchronously and nothing about -their ordering changes. The secure channel no longer serialises its state on a -monitor, and `UaSCBinaryChannel.DataLock` is `[Obsolete]`. The gate that replaced -it is not re-entrant, so the channel calls a lock-free `Core` variant on every -path that used to take the lock recursively. - -## By layer - -### Server - -`AsyncCustomNodeManager` is now the recommended base for all custom -NodeManagers, and every NodeManager shipped with the stack has migrated to -it. `MonitoredNode` is `IAsyncNodeManager`-aware and uses channel-based -queuing under load; events from the server node are dispatched through -multiple channel consumers; role-permission validation is cached -event-driven in `MonitoredNode2`; and the request queue, event handling, -and publishing path have all been hardened with new test coverage. Server -identity is now pluggable end-to-end (anonymous / username / X.509 / token -issuer) and persistent users are loaded through -`IUserDatabase.GetUsers`. Per OPC UA Part 3 §4.9 the server assigns the -`TrustedApplication` role automatically. The -[NodeManagement service set](NodeManagement.md), per-NodeManager `Allow…` -gates, and `ModelChangeAggregator` round out the server extensibility -surface. The server, the audit/redaction APIs, and the publishing path -have been audited for sync-over-async and converted to TAP. See -[Sessions](Sessions.md) for the matching session-/subscription-engine -story. +`services.AddOpcUa()` provides a common entry point for clients, servers, and +feature libraries. The .NET Generic Host manages server lifetime; options, +telemetry, identities, certificates, and application providers use the same +container. Direct construction remains available. +See [Dependency Injection](DependencyInjection.md). ### Client -The client now offers two coexisting paths. The -[classic `Session`](Sessions.md#1-session--the-opc-ua-session-primitive) -remains for callers that own session lifecycle. The -new [`ManagedSession`](Sessions.md#3-managedsession--the-connection-state-machine-facade) -encapsulates the connection state machine, reconnect policy, and pluggable -subscription engine behind a fluent builder; it is the recommended path for -new code. The **V2 subscription engine** runs alongside the classic engine -and has feature and test parity with it; an opt-in -`SubscriptionRecoveryPolicy` lets servers signal `Good_SubscriptionTransferred` -without surprising the client; and the user -token policy used on `Connect` is now re-used during `Reconnect` / -`ReactivateSession`. Under the hood, a new -**`IClientChannelManager`** owns client-side transport channels with -reference counting, sharing, and coalesced reconnect: sessions, discovery -clients, and registration clients targeting the same -`ConfiguredEndpoint` (with the same reverse-connect identity) share a -single underlying `ITransportChannel`, reconnect is transparent to -callers, and the previous `AttachChannel` / `DetachChannel` + -`SessionReconnectHandler` patterns are obsoleted. A shared `IRetryBudget` -collapses the two-level retry deadline so reconnect no longer compounds -multiplicatively. The new -[**unbounded monitored items**](Subscriptions.md) feature -transparently shards a V2 `ManagedSession` subscription across multiple -real server-side `Subscription` partitions when the server's -`MaxMonitoredItemsPerSubscription` cap is hit — callers that exceed the -cap continue to succeed instead of failing with -`BadTooManyMonitoredItems`, and the public `ISubscription` / -`MonitoredItemCollection` shape is unchanged. New client-side features -include [`FileSystemClient`](FileSystemClient.md) (a `System.IO`-style -async client over OPC UA File methods), -[`HistoryClient`](HistoricalAccess.md), -[`AlarmClient`](AlarmsAndConditions.md), and source-generated typed -ObjectType proxies. Client-side [NodeSet export](NodeSetExport.md) -extracts a server's address space to NodeSet2 XML, and -[`ModelChangeTracking`](ModelChangeTracking.md) keeps the local -`INodeCache` consistent with server-side model changes. +`ManagedSession` manages connection recovery behind a fluent builder while the +classic `Session` remains available for applications that own that lifecycle. +The V2 subscription API adds options-based callbacks, triggering relationships, +automatic partitioning, and `IAsyncEnumerable` streams for short-lived monitoring. +Typed helpers cover history, alarms, files, and model-change tracking. +See [Sessions](Sessions.md), [Subscriptions](Subscriptions.md), +[File System Client](FileSystemClient.md), and [Model Change Tracking](ModelChangeTracking.md). -### High availability and redundancy +### Source generators and modeling -2.0 maps OPC UA Part 4 §6.6 redundancy across the server, client, and network, -and layers an opt-in distributed high-availability story on top. On the server, -`AddServerRedundancy(...)` publishes the `Server.ServerRedundancy` nodes, drives -`Server.ServiceLevel`, advertises the `NTRS` non-transparent discovery -capability, and exposes `RequestServerStateChange` for administrator-driven -Maintenance/NoData failover — for every `RedundancySupport` mode -(None/Cold/Warm/Hot/HotAndMirrored/Transparent). On the client, a single -[`ManagedSession`](Sessions.md) with `WithServerRedundancy()` reads that -metadata and fails over transparently, so the same code works whether or not the -server is configured for redundancy. +ModelDesign and NodeSet2 inputs generate node-manager scaffolding, state types, +and typed ObjectType client proxies. POCO annotations generate `IEncodeable` +implementations, and model dependencies can be registered across assemblies. +Models that change independently of the binary can instead be loaded at runtime. +See [Node Managers](NodeManagers.md), [Source-Generated Data Types](SourceGeneratedDataTypes.md), +[Model Dependencies](ModelDependencies.md), and [Runtime NodeSets](RuntimeNodeSets.md). -The new `Opc.Ua.Redundancy`, `Opc.Ua.Redundancy.Server`, -`Opc.Ua.Redundancy.Client`, and `Opc.Ua.Redundancy.Kubernetes` packages add the -distributed building blocks behind DI/fluent seams. `UseDistributedAddressSpace` -/ `UseReplicatedAddressSpace`, `UseDistributedSessions` / -`UseReplicatedSessions`, and `UseDistributedSubscriptionMirroring` mirror -address-space topology and values, session state (fast reconnect that still runs -a full `ActivateSession` signature re-check against a single-use nonce), and -subscription / retransmission state across replicas. `UseRedundancyConsistency` -selects the shared store's consistency model — a leaderless **CRDT** gossip -layer (eventual, active/active) or a linearizable **Raft** layer -(`DefaultRaftConsensus`, strong, for single-use nonces and leader election) — -both over the in-package NanoMsg transport, with every record -authenticated-encrypted through `IRecordProtector`. Address-space hydration uses -a snapshot + delta-log fast path for quick time-to-ready on failover, and an -optional `GetEndpoints` load-direction seam (`UseServerLoadDirection`) can steer -clients to the best replica. The default path (no store configured) is unchanged -and zero-overhead. See [High Availability](HighAvailability.md) for the full -design and [Kubernetes](Kubernetes.md) for the deployment guide. +### Type system and immutability + +Readonly value types and the `Variant`-based API reduce boxing and make OPC UA +values explicit. `ByteString`, `ArrayOf`, and `MatrixOf` represent binary, +array, and matrix data. Runtime complex-type loading supports server-defined +structures and enumerations without requiring a generated CLR class for each type. +See [Complex Types](ComplexTypes.md) and [Schema Generation](SchemaGeneration.md). + +### Performance, memory, and pooling + +Value-type representations and pooled buffers reduce allocations in encoding and +notification paths. Server work also improves resource lifetime and repeated +permission checks. These changes do not imply that every workload is faster; +use the measured results and sizing guidance for your deployment. +See [Benchmarks](Benchmarks.md), [Server Scalability](ServerScalability.md), +and [Rate Limiting](RateLimiting.md). + +### Async, cancellation, and `TimeProvider` + +`AsyncCustomNodeManager` supports asynchronous service and data-source operations. +Request cancellation follows the request lifetime, while `TimeProvider` enables +controlled clocks and timers for testing. Existing synchronous compatibility APIs +do not make blocking waits appropriate in new asynchronous handlers. +See [Async Server Support](AsyncServerSupport.md) and [Node Managers](NodeManagers.md). + +### Native AOT + +Source-generated models, runtime type representations, and revised serialization +paths support applications published without a JIT compiler. Choose AOT-compatible +providers and the default complex-type builder; the optional Reflection.Emit +builder still requires runtime code generation. +See [Native AOT: publish an application](NativeAoT.md#publish-an-application) +and [Complex Types](ComplexTypes.md#type-builders). + +### OPC UA companion-spec coverage + +The SDK adds model libraries, hosting support, and client helpers for the following +services and domains. Core OPC UA specification parts are listed separately from +domain-specific companions and draft extensions. + +**Core specification services and models** + +| Capability | Specification | Guide | +| --- | --- | --- | +| Alarms and conditions | OPC 10000-9 | [Alarms and Conditions](AlarmsAndConditions.md) | +| Historical access and aggregates | OPC 10000-11 and OPC 10000-13 | [Historical Access](HistoricalAccess.md) and [Aggregates](Aggregates.md) | +| State machines | OPC 10000-16 | [State Machines](StateMachines.md) | +| Alias names | OPC 10000-17 | [Alias Names](AliasNames.md) | +| Node and reference management | OPC 10000-4 | [Node Management](NodeManagement.md) | +| File transfer and file systems | OPC 10000-5 Annex C and OPC 10000-20 | [File System Client](FileSystemClient.md) | +| Roles and user management | OPC 10000-18 | [Role-Based User Management](RoleBasedUserManagement.md) | +| Global discovery and certificate management (GDS) | OPC 10000-12 | [Global Discovery Server](GDS.md) | +| Local discovery (LDS and LDS-ME) | OPC 10000-12 | [LDS / LDS-ME](GDS.md#lds--lds-me-45) and the [Console LDS Server](../samples/Lds/ConsoleLdsServer) sample | +| Device onboarding: registrar ticket administration | OPC 10000-21 | [OnboardingClient](GDS.md#opc-10000-21-onboardingclient) and the [onboarding sample](../samples/Gds/README.md) | + +**Companion and extension models** + +| Domain | Specification or status | Guide | +| --- | --- | --- | +| Device composition and software updates | OPC 10000-100 (Devices) | [Device Integration](DeviceIntegration.md) and [Software Update](SoftwareUpdate.md) | +| Motion-device systems and controllers | OPC 40010-1 (Robotics), over OPC 10000-200 (Industrial Automation) | [Robotics](Robotics.md) | +| Task-level robot commands | Robot Intent (draft) | [Robot Intent](Robotics.md#robot-intent) | +| Manufacturing resources and job control | OPC 10030 (ISA-95 Common Model) and OPC 10031-4 (Job Control V1/V2) | [ISA-95](ISA95.md) | +| Relative and geographic positioning | OPC 10000-210 (RSL) and OPC 10000-211 (GPOS) | [Positioning](Positioning.md) | +| Perception, media, and feedback | Vision (draft) | [Vision](Vision.md) | +| Model catalogues, deployments, and inference | AI Model Management and Inference (draft) | [AI Model Management](AI.md) | +| Resource registration, versioning, and federation | xRegistry abstract base model (draft) | [xRegistry](XRegistry.md) | +| Scene bindings and address-space materialization | OpenUSD Bindings and OpenUSD Scene (both draft) | [OpenUSD](OpenUsd.md) | +| Web of Things connectivity and conversion | OPC 10100-1, with preview registry extensions | [WoT Connectivity](WoTConnectivity.md), [WoT / NodeSet Conversion](WoTNodeSetConversion.md), and [WoT Bindings](WotBindings.md) | + +The samples also generate **OPC 40223 (Pumps)** and **OPC 40001-1 (Machinery)** +models in the [pump server](../samples/DI/PumpDeviceIntegrationServer/README.md), +and the draft [Generators model](../samples/OpenUsd/GeneratorServer/Generators.md) +in the generator server. These are sample-local models, not additional standalone SDK libraries. + +Draft entries are identified by model name rather than an unverified specification +number; their namespace URIs and NodeIds remain provisional. The guides describe +provider requirements and supported operations. Model coverage does not imply +complete runtime implementation or certification for every specification. + +### Security and certificates + +Reference-counted certificates and `CertificateManager` provide explicit ownership, +trust-list management, rotation, and lifecycle notifications. Identity providers +support application-specific authentication. Pluggable crypto providers let an +application replace platform cryptography with another library, a remote service, +or hardware such as a TPM, HSM, or PKCS#11 token. Providers can be selected by +purpose and security policy, including support for hardware-held private keys. +ECC SecureChannel and user-token policies require .NET 8-or-later stack assets; +curve and cipher availability also depends on the platform. +See [Certificate Manager](CertificateManager.md), [Identity Providers](IdentityProviders.md), +[Crypto Providers](CryptoProvider.md), and [ECC Profiles](EccProfiles.md#known-limitations). ### Global Discovery Server -The GDS implementation is now **full OPC UA Part 12 compliance**, including -modern `StartRequestToken` / `FinishRequestToken` flows -([AuthorizationService](AuthorizationService.md)) and the pull/push -[KeyCredentialService](KeyCredentialService.md). The client supports -pushing to arbitrary certificate groups; the server supports custom -certificate groups; SubCAs can be revoked without auto-creating an empty -CRL; and method-call validation is strict. The full developer guide -is in [GDS](GDS.md). +GDS support extends application registration, pull/push certificate management, +custom certificate groups, token issuance, and credential services. A Local +Discovery Server is also available as a hosted component or standalone sample. +For OPC 10000-21 device onboarding, clients register and unregister tickets with +a device registrar through `OnboardingClient`, and the registrar keeps them in a +pluggable `ITicketStore`. +See [GDS](GDS.md) for capability boundaries and conformance evidence, +[Authorization Service](AuthorizationService.md), and [Key Credential Service](KeyCredentialService.md). ### Part 14 PubSub modernization -The PubSub stack (`Opc.Ua.PubSub`, `Opc.Ua.PubSub.Udp`, -`Opc.Ua.PubSub.Mqtt`, `Opc.Ua.PubSub.Server`) is rewritten end-to-end -to track [Part 14 v1.05.06](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06). +A fluent builder and dependency-injection extensions compose PubSub applications +from separate runtime, transport, and server-integration packages. UADP and JSON +encoding, discovery, retained metadata, runtime configuration, diagnostics, and +Security Key Service integration support publisher/subscriber workflows. +Transport and cryptographic capabilities remain platform-dependent; in particular, +the DTLS Curve25519 / Curve448 profiles are not registered. +See [PubSub](PubSub.md) for supported profiles, transports, and examples. -- **Native AOT clean.** The combined reference sample - (`ConsoleReferencePubSubClient`, with `publisher` / `subscriber` / `external` modes) - publishes AOT with zero `IL2026` / `IL3050`; `PubSubAotTests` exercises every - runtime path under AOT. -- **DI-integrated.** `services.AddOpcUa().AddPubSub(o => …)` registers - the runtime, scheduler, security subsystem, transports, and SKS into - the standard `IServiceCollection`. See - [`DependencyInjection.md`](DependencyInjection.md). The previous - "PubSub is not part of the dependency-injection surface" caveat is - removed. -- **Fluent builder.** `PubSubApplicationBuilder` composes connections, - groups, writers, readers, transports, and security in code; XML - configuration loads through the same builder. Inline construction or - full `IPubSubConfigurationStore` round-tripping are equivalent. -- **Full v1.05.06 spec coverage.** UADP (§7.2.4) and JSON (§7.2.5) - encoders/decoders, including `JsonEncodingMode` { Verbose, Compact, - RawData }, `SingleNetworkMessage`, Action and Discovery messages; - UDP datagram-v2 (`DatagramConnectionTransport2DataType` + - `QosCategory` → DSCP), MQTT 3.1.1 + 5.0 with QoS 0/1/2 and retained - metadata at startup; SKS pull / push (§8.5.1, §8.5.2); AES-128-CTR - and AES-256-CTR with HMAC-SHA-256 (NIST SP 800-38A F.5.1 / F.5.5 - KAT-asserted). -- **Per-component diagnostics.** Every connection, group, writer, and - reader now carries its own `IPubSubDiagnostics` instance, surfaced - on the address space when `AddPubSubAddressSpace` is wired. -- **Runtime configuration mutation.** `IPubSubApplication.Add/Remove*` - methods compose new connections / groups / writers / readers without - a stop-reconfigure-restart cycle; the same methods are bound to the - Part 14 §9 `PublishSubscribe` Object methods. -- **Security wiring.** `UadpSecurityWrapper` is now invoked at send / - receive; configurations that named a `SecurityMode` other than - `None` actually get that security applied. -- **Retained metadata.** `MetaDataPublisher` advertises every active - `DataSetMetaData` once at startup (UDP discovery / MQTT retained - topic) so subscribers that join mid-stream can decode RawData. -- **MQTT 3.1.1 + 5.0.** The MQTT transport uses `MQTTnet` v4 on net48 - / netstandard and v5 on net8 / 9 / 10. Certificate authentication - per [Part 14 §6.4.2.2.4](https://reference.opcfoundation.org/specs/OPC-10000-14/v1.05.06/6.4.2.2.4) - is supported. +### High availability and redundancy -For library reference and code samples, read [`PubSub.md`](PubSub.md). -For the upgrade story (breaking changes, compatibility matrix, and -codemod recipes), read -[`migrate/2.0.x/pubsub.md`](migrate/2.0.x/pubsub.md). +Server redundancy metadata and `ManagedSession` failover support OPC UA redundancy +workflows. Optional distributed stores add address-space, session, and subscription +mirroring with eventual or strong consistency choices. Kubernetes integration +provides deployment, discovery, and readiness support. +See [High Availability](HighAvailability.md), [Replica-Consistent NodeIds](ReplicaNodeIdentity.md), +and [Kubernetes](Kubernetes.md) for configuration and deployment constraints. -### Tooling +### Tooling and diagnostics -A new **MCP server** (see [MCP Server](McpServer.md)) exposes OPC UA client -operations as Model Context Protocol tools, so an LLM or Copilot can -browse, read, write, subscribe, and call methods on any OPC UA server. -The MCP server additionally ships **packet-capture, packet-decode, and -packet-replay** tools so the agent can capture a UA-TCP exchange to -PCAP, inspect the decoded service calls, and replay them — useful for -reproducing interop issues without re-running the original client. A -**2.0 Migration Analyzer + code fixer** ships as a Roslyn analyser package -(`OPCFoundation.NetStandard.Opc.Ua.MigrationAnalyzer`) that detects the -typical 1.5.378 → 2.0 patterns and applies most of the mechanical edits -automatically; see the [Migration Guide](MigrationGuide.md) for the -opt-in workflow. - -### Build, CI, and observability - -The build pipeline now runs on a managed DevOps pool with a per-TFM -build/test matrix and parallelized client tests. `Nullable` is enabled -across `src/`, the nine `src/`, and the `samples/` projects; -dispose-analyzers (`CA2000`, `CA2213`) are on and clean. The repository -follows a strict `dotnet format` baseline (whitespace, IDE, RCS) enforced -by the `opc-ua-codestyle-enforcer` agent. Code analysis runs at "preview" -level with "all" mode, package validation is on, and treat-warnings-as- -errors is set repo-wide. On the runtime side, -[Diagnostics](Diagnostics.md) is plumbed through `ITelemetryContext`: -loggers, meters, and activities all hang off the same context object, and -log redaction is wired through the audit APIs. Tests have been -reorganised for faster CI, with several integration suites separated from -unit suites, and code-coverage gates apply to all non-test, non-application -projects. Those gates now run inside the pipeline itself - an absolute -project floor plus a changed-lines check - instead of relying on an external -coverage service; see -[Continuous integration](DeveloperGuide.md#continuous-integration). +The MCP server exposes client operations and packet-capture tools to agents. +`ITelemetryContext` supplies application logging, metrics, and tracing, with +audit and redaction support. A migration analyzer and code fixer assist with +mechanical API updates. +See [MCP Server](McpServer.md) and [Diagnostics](Diagnostics.md). ## Further reading -- [Migration Guide](MigrationGuide.md) — prescriptive, per-API migration - reference from 1.5.378 to 2.0. -- [Profiles](Profiles.md) — supported OPC UA profiles, facets, and - security policies in 2.0. -- [Sessions, Reconnection, and Subscription Engines](Sessions.md) — - architectural overview of `Session` vs `ManagedSession` and the - classic vs V2 subscription engines. -- [High Availability and Redundancy](HighAvailability.md) — OPC 10000-4 §6.6 - server/client/network redundancy and the opt-in distributed HA building - blocks; [Kubernetes High Availability Deployment](Kubernetes.md) — the - Kubernetes deployment guide for the `Opc.Ua.Redundancy.Kubernetes` package. -- [NodeId Assignment](NodeIdAssignment.md) — how runtime NodeIds are - minted: the factory contract, identifier formats, types vs instances, - the generated helpers, and per-NodeManager behaviour. -- [Dependency Injection](DependencyInjection.md), - [Native AOT](NativeAoT.md), - [Diagnostics](Diagnostics.md), - [Source-Generated NodeManagers](NodeManagers.md#source-generated-node-managers), - [Source-Generated DataTypes](SourceGeneratedDataTypes.md). -- Companion specs: - [Alarms and Conditions](AlarmsAndConditions.md), - [Historical Access](HistoricalAccess.md), - [Aggregates](Aggregates.md), - [State Machines](StateMachines.md), - [Alias Names](AliasNames.md), - [Device Integration](DeviceIntegration.md), - [Relative Spatial Location and Global Positioning](Positioning.md), - [Vision](Vision.md), - [AI Model Management](AI.md), - [Software Update](SoftwareUpdate.md), - [WoT Connectivity](WoTConnectivity.md), - [Subscriptions and Monitored Items](Subscriptions.md), - [Node Management](NodeManagement.md), - [Model Change Tracking](ModelChangeTracking.md), - [Model Dependencies](ModelDependencies.md). -- Security, identity, and certificates: - [Certificates](Certificates.md), - [Certificate Manager](CertificateManager.md), - [ECC Profiles](EccProfiles.md), - [Role-Based User Management](RoleBasedUserManagement.md), - [Identity Providers](IdentityProviders.md), - [Authorization Service](AuthorizationService.md), - [Key Credential Service](KeyCredentialService.md), - [GDS Developer Guide](GDS.md). -- Client features: - [File System Client](FileSystemClient.md), - [NodeSet Export](NodeSetExport.md), - [Complex Types](ComplexTypes.md), - [Transfer Subscription](TransferSubscription.md), - [Reverse Connect](ReverseConnect.md), - [Durable Subscription](DurableSubscription.md). -- Tooling: [MCP Server](McpServer.md), - [Container Reference Server](ContainerReferenceServer.md), - [Provisioning Mode](ProvisioningMode.md). -- PubSub: [PubSub library](PubSub.md). +- [Migration Guide](MigrationGuide.md) — breaking changes, per-API replacements, + and the migration analyzer workflow for upgrading from 1.5.378. +- [Documentation index](README.md) — task-based routes into the SDK guides. +- [Sample applications](Samples.md) — runnable client, server, PubSub, and + companion-model examples. +- [Profiles and facets](Profiles.md) — capability and platform overview. diff --git a/docs/WoTConnectivity.md b/docs/WoTConnectivity.md index e6b56fa9de..6a1911ab4c 100644 --- a/docs/WoTConnectivity.md +++ b/docs/WoTConnectivity.md @@ -16,6 +16,44 @@ target version `1.02.0`, publication 2025-12-05. For current protocol-runtime architecture and the contributor guide for adding a protocol see [WoT protocol bindings](WotBindings.md), and the runnable end-to-end topology is documented in the [WoT aggregation sample](../samples/WotCon/README.md). +## Contents + +- [1. Hosting a WoT Connectivity server](#1-hosting-a-wot-connectivity-server) + - [Lifecycle](#lifecycle) + - [Mirroring assets into the WoT xRegistry](#mirroring-assets-into-the-wot-xregistry) +- [2. Writing a custom `IWotAssetProvider`](#2-writing-a-custom-iwotassetprovider) + - [Event affordances](#event-affordances) +- [3. Using the client](#3-using-the-client) + - [FileSystem extensions](#filesystem-extensions) + - [Method invocation and server interoperability](#method-invocation-and-server-interoperability) +- [4. Persistence limits](#4-persistence-limits) +- [5. Name validation](#5-name-validation) +- [6. Endpoint policy](#6-endpoint-policy) + - [The generated Thing Description is untrusted too](#the-generated-thing-description-is-untrusted-too) +- [7. Error reporting](#7-error-reporting) +- [8. Security: management access policy](#8-security-management-access-policy) +- [9. Limitations and known issues](#9-limitations-and-known-issues) +- [10. References](#10-references) +- [11. WoT Connectivity 1.1 registry and materialization (preview)](#11-wot-connectivity-11-registry-and-materialization-preview) + - [11.1 Architecture](#111-architecture) + - [11.2 Registry service and persistence](#112-registry-service-and-persistence) + - [11.3 Materialization coordinator](#113-materialization-coordinator) + - [11.4 Binder integration seam](#114-binder-integration-seam) + - [11.5 Legacy 1.02 compatibility](#115-legacy-102-compatibility) + - [11.6 Protocol and projection scope](#116-protocol-and-projection-scope) + - [11.7 Browseable registry projection and management Methods](#117-browseable-registry-projection-and-management-methods) + - [11.8 Binding-vocabulary alignment (NodeSet2 ↔ WoT)](#118-binding-vocabulary-alignment-nodeset2--wot) + - [11.9 Registry client](#119-registry-client) +- [12. Conformance to WoT Connectivity 1.1](#12-conformance-to-wot-connectivity-11) + - [12.1 Model identity](#121-model-identity) + - [12.2 Conformance units and profiles](#122-conformance-units-and-profiles) + - [12.3 Grouping](#123-grouping) + - [12.4 Projection documents and the View NodeClass](#124-projection-documents-and-the-view-nodeclass) + - [12.4.1 Parent placement through `uav:componentOf`](#1241-parent-placement-through-uavcomponentof) + - [12.4.2 Event notifier behaviour in projection Views](#1242-event-notifier-behaviour-in-projection-views) + - [12.5 Portable identifiers](#125-portable-identifiers) + - [12.6 The 1.02 asset surface](#126-the-102-asset-surface) + --- ## 1. Hosting a WoT Connectivity server diff --git a/docs/WoTNodeSetConversion.md b/docs/WoTNodeSetConversion.md index 3ea30a2414..477479b5ee 100644 --- a/docs/WoTNodeSetConversion.md +++ b/docs/WoTNodeSetConversion.md @@ -7,6 +7,35 @@ when requested, uses the structured `uav:nodes` projection when the readable vocabulary is not complete, and otherwise synthesizes NodeSet2 from the readable WoT terms. +## Contents + +- [Which specification revision this tracks](#which-specification-revision-this-tracks) +- [Importable output](#importable-output) + - [Modelling rules and the two placeholder identifiers](#modelling-rules-and-the-two-placeholder-identifiers) +- [WoT to NodeSet defaults](#wot-to-nodeset-defaults) +- [Portable identity and preservation semantics](#portable-identity-and-preservation-semantics) + - [Verified linked document sets](#verified-linked-document-sets) + - [Generated NodeIds follow Annex G.1](#generated-nodeids-follow-annex-g1) + - [Preservation digests and the two things that can be measured](#preservation-digests-and-the-two-things-that-can-be-measured) + - [Unmapped reference vocabulary is residue](#unmapped-reference-vocabulary-is-residue) + - [Condition events derive from their ConditionType](#condition-events-derive-from-their-conditiontype) + - [Event fields come from the type, not only from the document](#event-fields-come-from-the-type-not-only-from-the-document) + - [Resolving a stated selection needs the asynchronous conversion](#resolving-a-stated-selection-needs-the-asynchronous-conversion) +- [DataType definitions (Section 6.11)](#datatype-definitions-section-611) +- [Model and platform vocabulary (Section 6)](#model-and-platform-vocabulary-section-6) +- [Conformance claims and strict mode (Sections 4.1, 6.1, 6.6 and 11)](#conformance-claims-and-strict-mode-sections-41-61-66-and-11) + - [Authoring a claim and processing one are different acts](#authoring-a-claim-and-processing-one-are-different-acts) + - [The `uav:nodes` record grammar is not vocabulary](#the-uavnodes-record-grammar-is-not-vocabulary) + - [Generated documents state the revision they were generated against](#generated-documents-state-the-revision-they-were-generated-against) + - [NodeClass and ReferenceType vocabulary](#nodeclass-and-referencetype-vocabulary) +- [Engineering units, ranges and scaling (Sections 6.4 and 6.4.1)](#engineering-units-ranges-and-scaling-sections-64-and-641) +- [Localized text (Section 9.1.1)](#localized-text-section-911) +- [ValueRank and ArrayDimensions (Sections 7 and 9.1)](#valuerank-and-arraydimensions-sections-7-and-91) +- [Method arguments (Section 9.1)](#method-arguments-section-91) +- [ReferenceTypes and relations (Sections 5.1.2, 5.3 and 6.2)](#referencetypes-and-relations-sections-512-53-and-62) + - [`uav:componentOf` and its `ua:ComponentOf` alias](#uavcomponentof-and-its-uacomponentof-alias) + - [Projection documents and the OPC 10100-1 v1.02 asset surface](#projection-documents-and-the-opc-10100-1-v102-asset-surface) + ## Which specification revision this tracks This implementation tracks **WoT Binding revision 1.1**. The revision is @@ -112,7 +141,7 @@ them. | Property affordance `uav:browseName` | **Default** | The affordance map key is used as the local name and BrowseName `1:`. | | Property affordance `uav:id` | **Default** | Deterministic NodeId by Annex G.1: `ns=1;s=/nsu=;/nsu=;`. | | Property DataSchema `type` or an unrecognized `type` | **Default** | The canonical table of WoT Binding §6.11.4: `boolean` → `Boolean`, `integer` → the **abstract** `Integer` (`i=27`), `number` → the **abstract** `Number` (`i=26`), `string` → `String`, refined by `contentEncoding: base64` → `ByteString`, `format: date-time` → `DateTime`, `format: uuid` → `Guid`, `format: uri` → `UriString`. An explicit `uav:dataTypeId` or `uav:mapToType` outranks the inference. Anything unrecognized falls back to `BaseDataType` (`i=24`). A bare `integer` or `number` is deliberately abstract: the schema states only that the value is whole or numeric, and a concrete width is recovered from an annotation rather than guessed. | -| Property `readOnly` and `writeOnly` | **Default** | Missing flags mean read/write access (`CurrentRead | CurrentWrite`, value `3`). If both flags are `true`, the zero-access result is coerced to `CurrentRead` (`1`); this is an arbitrary safety default and should be specified explicitly. | +| Property `readOnly` and `writeOnly` | **Default** | Missing flags mean read/write access (`CurrentRead \| CurrentWrite`, value `3`). If both flags are `true`, the zero-access result is coerced to `CurrentRead` (`1`); this is an arbitrary safety default and should be specified explicitly. | | Property `title` | **Default** | No `DisplayName` field is materialized for the variable. A `titles` map materializes one `LocalizedText` per locale, the default locale's entry first (Section 9.1.1). | | Property `description` | **Default** | No `Description` field is materialized for the variable. A `descriptions` map materializes one `LocalizedText` per locale. | | Property `uav:valueRank` (Sections 7, 9.1) | **Default** / **Fails** | Absent: `ValueRank` `-1` (Scalar), which is what a NodeSet omits. Present: the stated rank, so `-3`, `-2`, `-1`, `0` and a fixed positive rank stay distinct. Not an integer literal, or below `-3`: `InvalidValueRank` error. | @@ -490,7 +519,7 @@ handles them in one direction with full round-trip fidelity: native projection preserves it losslessly, and any residue previously captured for a term is re-applied by JSON Pointer. - **Round-trip.** A document carrying these terms survives - WoT → NodeSet → WoT unchanged. Affordance-level terms + WoT → NodeSet → WoT unchanged. Affordance-level terms (`uav:scaleFactor`, `uav:decimalPlaces`, `uav:semanticId`) are preserved under the affordance's projected local name, so an affordance that also carries `uav:browseName` round-trips under that browse name's diff --git a/docs/WotBindings.md b/docs/WotBindings.md index 98d7e002a2..72061dfe43 100644 --- a/docs/WotBindings.md +++ b/docs/WotBindings.md @@ -14,6 +14,7 @@ This document starts with the bindings that ship today and how to register them, - [Polling, retry and backoff](#polling-retry-and-backoff) - [Runtime integration](#runtime-integration) - [OPC UA target-mapping binding runtime](#opc-ua-target-mapping-binding-runtime) + - [Projected Methods, events, and Conditions](#projected-methods-events-and-conditions) - [Registering binders and executors](#registering-binders-and-executors) - [Intentionally unsupported operations](#intentionally-unsupported-operations) - [Transport security](#transport-security) @@ -1153,30 +1154,22 @@ neither becomes a Node, and both are restated verbatim on a round trip. ### What the readable mapping does not yet carry -Section 9.2 emits the exceptional `uav:nodes` projection where converting the readable -document back would not reproduce an equivalent NodeSet. Two gaps in this -implementation still trigger it, both ordinary work rather than limits of the -vocabulary. +Readable mappings and their fallback conditions are maintained in +[WoT / NodeSet conversion](WoTNodeSetConversion.md). In particular, +[engineering units, ranges and scaling](WoTNodeSetConversion.md#engineering-units-ranges-and-scaling-sections-64-and-641) +are mapped in both directions when their values can be decoded. Missing or +unsupported values use the documented preservation projection rather than +invented defaults. -A Variable's own Variable children - the `EURange` and `EngineeringUnits` Properties of -an `AnalogUnitType` - sit one level deeper than the conversion descends, so they are -not emitted. And a Variable's `Value` is carried only where it is a scalar the -conversion special-cases; a structure is not carried at all. +Use this guide for protocol planners, executors, and binding registration; +use [WoT Connectivity](WoTConnectivity.md) for registry hosting and projection +lifecycle. Keeping conversion rules in one place avoids binding implementations +depending on an outdated list of mapping limitations. -Neither needs new vocabulary. A structure's value is self-describing: the -`ExtensionObject` states the identifier of the type it holds, `EUInformation` and -`Range` are types this stack already generates from the standard NodeSet, and the -encoder stack in `Opc.Ua.Types/Encoders` maps such a value to named JSON fields and -back. Nothing has to infer a unit's identifier from its symbol. - -One convention is worth knowing when reading a generated document: completeness is -tested with `NodeSetComparer.CompareEquivalent`, which reads each side through its own -`Aliases` table, because Section 9.2 asks for an equivalent NodeSet and not an -identically spelled one. A name neither side declares is read through the -`INodeSetAliasResolver` the caller injects — here `WotNodeSetAliases`, which states -that the Binding writes the standard base-namespace names — so the comparison itself -states no policy of its own. `NodeSetComparer.Compare` keeps the stricter text -comparison for callers that need to know a document was reproduced as written. +Equivalence and byte identity are different contracts. `NodeSetComparer.CompareEquivalent` +resolves each NodeSet's aliases, while `NodeSetComparer.Compare` retains stricter +text comparison. See the conversion guide for readable, preservation, and exact +round-trip guarantees. ### How this is checked diff --git a/docs/XRegistry.md b/docs/XRegistry.md index 15d512eca0..703a4657f7 100644 --- a/docs/XRegistry.md +++ b/docs/XRegistry.md @@ -12,6 +12,27 @@ everything else. The PubSub Schema Registry and WoT Connectivity registry are co specializations: PubSub resources are schema documents, while WoT resources are Thing Description / Thing Model documents. +## Contents + +- [Packages](#packages) +- [Core concepts](#core-concepts) + - [Structural identity and content lookup](#structural-identity-and-content-lookup) + - [Opaque-NodeId fast path](#opaque-nodeid-fast-path) + - [Registration lifecycle and auto-bootstrap](#registration-lifecycle-and-auto-bootstrap) + - [File open modes](#file-open-modes) + - [Resource storage](#resource-storage) + - [Transport security](#transport-security) + - [Federation](#federation) + - [Labels](#labels) + - [Native xRegistry events](#native-xregistry-events) +- [Server-side usage](#server-side-usage) + - [Async lifecycle](#async-lifecycle) + - [Resource-exhaustion bounds](#resource-exhaustion-bounds) +- [Client-side usage](#client-side-usage) + - [Extending for a domain registry](#extending-for-a-domain-registry) +- [Well-known identifiers](#well-known-identifiers) +- [Related documentation](#related-documentation) + ## Packages | Package | Depends on | Contains | @@ -167,62 +188,22 @@ model on top of the base one, mirroring how a domain client derives from `XRegis #### Implementing a store -A store has four operations. The example below is a complete, if naive, implementation that keeps each -document in a dictionary — enough to show what each contract clause means: +Use the in-box `InMemoryResourceStore` or `FileSystemResourceStore` unless the +application requires another persistence backend. A custom `IXRegistryResourceStore` +must implement four operations: -```csharp -public sealed class MyResourceStore : IXRegistryResourceStore -{ - public ValueTask ReadAsync( - string resourceKey, long offset, int count, CancellationToken ct = default) - { - // Argument faults throw; an unknown key is a *null* ByteString so the caller can tell - // "no such resource" from "resource is empty". - if (!m_documents.TryGetValue(resourceKey, out byte[]? document)) - { - return new ValueTask(default(ByteString)); - } +| Operation | Contract | +| --- | --- | +| `ReadAsync` | Read from the requested offset, returning fewer bytes at the end. Return a null `ByteString` for an unknown key, and an empty non-null value for an empty read of an existing resource. | +| `WriteAsync` | Replace the addressed byte range without truncating the remainder. Extend the document and zero-fill any gap when writing beyond its end. | +| `GetLengthAsync` | Return the byte length, or `-1` for an unknown key. | +| `DeleteAsync` | Return whether a resource was deleted; an absent key returns `false`. | - // Return fewer bytes than asked for at the end of the document; never throw for that. - if (offset >= document.Length || count == 0) - { - return new ValueTask(ByteString.From([])); - } - int take = (int)Math.Min(count, document.Length - offset); - return new ValueTask( - ByteString.From(document.AsSpan((int)offset, take).ToArray())); - } - - public ValueTask WriteAsync( - string resourceKey, long offset, ByteString data, CancellationToken ct = default) - { - // Random access: the chunk may land anywhere. Writing past the end grows the document and - // the gap reads back as zeros; writing at 0 does *not* truncate what follows. - m_documents.TryGetValue(resourceKey, out byte[]? existing); - existing ??= []; - var merged = new byte[Math.Max(existing.Length, offset + data.Length)]; - existing.CopyTo(merged.AsSpan()); - data.Span.CopyTo(merged.AsSpan((int)offset)); - m_documents[resourceKey] = merged; - return default; - } - - public ValueTask GetLengthAsync(string resourceKey, CancellationToken ct = default) - { - // -1 signals an unknown key rather than throwing. - return new ValueTask( - m_documents.TryGetValue(resourceKey, out byte[]? d) ? d.Length : -1); - } - - public ValueTask DeleteAsync(string resourceKey, CancellationToken ct = default) - { - // Deleting an absent key is a no-op, not a fault. - return new ValueTask(m_documents.Remove(resourceKey)); - } - - private readonly Dictionary m_documents = []; -} -``` +For implementation examples, see +[`InMemoryResourceStore`](../src/Opc.Ua.XRegistry.Server/InMemoryResourceStore.cs) +and [`FileSystemResourceStore`](../src/Opc.Ua.XRegistry.Server/FileSystemResourceStore.cs). +Concurrent writes must preserve non-overlapping updates. A concurrent dictionary +alone does not make a read-modify-write sequence atomic. Two rules make a store substitutable: diff --git a/docs/migrate/2.0.x/README.md b/docs/migrate/2.0.x/README.md index 11d9147a29..5628e18fcc 100644 --- a/docs/migrate/2.0.x/README.md +++ b/docs/migrate/2.0.x/README.md @@ -25,6 +25,12 @@ find the sub-doc that matches the symptom you are seeing. > skill, which knows when to load each sub-doc and runs the codefixer > for you. +## Contents + +- [Migration sub-doc index](#migration-sub-doc-index) +- [All sub-documents](#all-sub-documents) +- [See also](#see-also) + ## Migration sub-doc index Find the row that matches the error / API surface you are dealing with diff --git a/docs/migrate/2.0.x/alarms-model-change.md b/docs/migrate/2.0.x/alarms-model-change.md index aa81ccceb0..628a33f784 100644 --- a/docs/migrate/2.0.x/alarms-model-change.md +++ b/docs/migrate/2.0.x/alarms-model-change.md @@ -2,6 +2,14 @@ > **When to read this:** Read this for `AlarmConditionState` state-transition behaviour, the auto-emitted `GeneralModelChangeEvent` from `CustomNodeManager.CreateNode/DeleteNode`, and the address-space `ModelChangeAggregator` migration. +## Contents + +- [Alarms and Conditions](#alarms-and-conditions) + - [`AlarmConditionState` state-transition behavior](#alarmconditionstate-state-transition-behavior) + - [Auto-emit `GeneralModelChangeEvent` from `CustomNodeManager`](#auto-emit-generalmodelchangeevent-from-customnodemanager) +- [Address-space model change tracking](#address-space-model-change-tracking) + - [New `INodeCache.InvalidateNode` member](#new-inodecacheinvalidatenode-member) + ## Alarms and Conditions Two changes require attention. diff --git a/docs/migrate/2.0.x/certificates.md b/docs/migrate/2.0.x/certificates.md index 189bea430d..8b80038043 100644 --- a/docs/migrate/2.0.x/certificates.md +++ b/docs/migrate/2.0.x/certificates.md @@ -2,6 +2,21 @@ > **When to read this:** Read this for the new ref-counted `Certificate` wrapper, the segregated-interface `CertificateManager` design, the `ICertificateProvider` cache, and the obsoleted `X509Certificate2` direct-exposure APIs. +## Contents + +- [Centralised certificate cache via `ICertificateProvider`](#centralised-certificate-cache-via-icertificateprovider) +- [Certificate Management](#certificate-management) + - [ECC security policies require .NET 8 or later](#ecc-security-policies-require-net-8-or-later) + - [Certificates with an empty distinguished name are always rejected](#certificates-with-an-empty-distinguished-name-are-always-rejected) + - [Certificate and CertificateCollection wrapper types](#certificate-and-certificatecollection-wrapper-types) + - [CertificateManager and segregated interfaces](#certificatemanager-and-segregated-interfaces) + - [CertificateIdentifier is metadata-only](#certificateidentifier-is-metadata-only) + - [CertificateStoreIdentifier is a store description — `OpenStore` returns a caller-owned store](#certificatestoreidentifier-is-a-store-description--openstore-returns-a-caller-owned-store) + - [PushManagement transactions: TrustList/Certificate updates now require `ApplyChanges`](#pushmanagement-transactions-trustlistcertificate-updates-now-require-applychanges) + - [Optional ServerConfiguration surface now available (additive, opt-in)](#optional-serverconfiguration-surface-now-available-additive-opt-in) + - [`MaxTrustListSize` is advertised honestly and bounded by a safety ceiling](#maxtrustlistsize-is-advertised-honestly-and-bounded-by-a-safety-ceiling) + - [Obsoleted certificate APIs](#obsoleted-certificate-apis) + ## Centralised certificate cache via `ICertificateProvider` A new public `ICertificateProvider` interface exposes the existing @@ -235,7 +250,7 @@ through the new `ServerConfigurationOptions` argument on Previously, a `ServerConfiguration.MaxTrustListSize` of `0` (unlimited per OPC UA Part 12 §8.4.5) was advertised to Clients as `0` while the server silently -enforced a hidden 1 MiB cap on TrustList `Read`/`Write`. The advertised +enforced a hidden 1 MiB cap on TrustList `Read`/`Write`. The advertised value and the enforced value could therefore disagree. The server now: @@ -243,17 +258,17 @@ The server now: - **Advertises the honest effective limit.** `ServerConfiguration.MaxTrustListSize` now reports the value the TrustList handlers actually enforce — never `0` while a finite cap is in force. A server configured with `MaxTrustListSize = 0` - now advertises the safety ceiling (default 1 MiB) instead of `0`. + now advertises the safety ceiling (default 1 MiB) instead of `0`. - **Adds a configurable resource-protection safety ceiling.** `ServerConfigurationOptions.MaxTrustListSizeSafetyCeiling` (default - 1 MiB) bounds the actually-enforced size. The effective limit is: + 1 MiB) bounds the actually-enforced size. The effective limit is: `MaxTrustListSize == 0` → the ceiling; `MaxTrustListSize` above the ceiling → the ceiling; otherwise → the configured `MaxTrustListSize`. **Migration action.** No action is required for the common cases -(`MaxTrustListSize` of `0` or a finite value ≤ 1 MiB); enforcement is +(`MaxTrustListSize` of `0` or a finite value ≤ 1 MiB); enforcement is unchanged and only the advertised value becomes honest. **If you configured a -finite `MaxTrustListSize` larger than 1 MiB**, raise +finite `MaxTrustListSize` larger than 1 MiB**, raise `ServerConfigurationOptions.MaxTrustListSizeSafetyCeiling` to at least that value, otherwise the effective limit is clamped to the ceiling: @@ -267,12 +282,12 @@ builder.ConfigureServerConfiguration(o => The legacy `TrustList` constructor overloads (without an explicit safety ceiling) are unchanged and remain fully backward compatible: a finite size is -honored exactly (never clamped) and `0` falls back to the 1 MiB default. +honored exactly (never clamped) and `0` falls back to the 1 MiB default. ### Obsoleted certificate APIs -The following APIs are marked `[Obsolete]` and will be removed in the next minor version. They remain -functional forwarders to the new design for binary-compatibility, but emit `CS0618` warnings when used. +The following static factory methods remain `[Obsolete]` forwarders and emit +`CS0618` when used. Migrate them to the instance-based factory and issuer APIs. | Obsolete API | Replacement | |-------------|-------------| @@ -283,16 +298,25 @@ functional forwarders to the new design for binary-compatibility, but emit `CS06 | `CertificateFactory.RevokeCertificate(...)` | `DefaultCertificateIssuer.Instance.RevokeCertificates(...)` | | `CertificateFactory.CreateCertificateWithPEMPrivateKey(...)` | `DefaultCertificateFactory.Instance.CreateWithPEMPrivateKey(...)` | | `CertificateFactory.CreateCertificateWithPrivateKey(...)` | `DefaultCertificateFactory.Instance.CreateWithPrivateKey(...)` | -| `CertificateStoreIdentifier.RegisterCertificateStoreType(...)` | Register `ICertificateStoreProvider` via dependency injection or pass to the `CertificateManager` constructor | +| `CertificateStoreType.RegisterCertificateStoreType(...)` | Register `ICertificateStoreProvider` through dependency injection or pass it to `CertificateManager`. This obsolete registration API is not a factory forwarder. | + +#### Removed certificate APIs + +The following types and properties are removed, not obsolete forwarders. +Update callers before compiling against 2.0. + +| Removed API | Replacement | +|-------------|-------------| | `CertificateValidator` (class) | `ICertificateManager` (composed of `ICertificateValidatorEx` for validation, `ICertificateRegistry` for app certs, `ICertificateTrustListManager` for trust lists, `ICertificateLifecycle` for change events). Construct via `CertificateManagerFactory.Create(securityConfiguration, telemetry, ...)` | | `ICertificateValidator` (interface) | `ICertificateValidatorEx` from `ICertificateManager`. The new interface returns a structured `CertificateValidationResult` (`IsValid`, `StatusCode`, `Errors`, `IsBeingTrustedTransiently`) instead of throwing. Per-error accept logic moves from the `CertificateValidation` event to the new `CertificateValidationOptions.AcceptError` callback. | | `CertificateTypesProvider` (class) | `ICertificateRegistry` (composed in `ICertificateManager`). Use `using CertificateEntry? e = manager.AcquireApplicationCertificateBySecurityPolicy(securityPolicyUri);` (caller-owned — dispose the entry). The entry already carries the chain: use `e.IssuerChain` / `e.GetEncodedChainBlob()`. | -| `ApplicationConfiguration.CertificateValidator` (property) | `ApplicationConfiguration.CertificateManager` (parallel property — set in `ApplicationInstance.CheckApplicationInstanceCertificatesAsync`) | +| `CertificateValidatorAdapter` (class) | No legacy bridge; consume `ICertificateValidatorEx` or `ICertificateManager` directly. | +| `ApplicationConfiguration.CertificateValidator` (property) | `ApplicationConfiguration.CertificateManager` (set in `ApplicationInstance.CheckApplicationInstanceCertificatesAsync`) | | `ServerBase.CertificateValidator` (property) | `ServerBase.CertificateManager` | | `ServerBase.InstanceCertificateTypesProvider` (property) | `ServerBase.CertificateManager` (use `ICertificateRegistry` surface) | > **Lifecycle ordering.** `configuration.CertificateManager` is populated *inside* `await applicationInstance.CheckApplicationInstanceCertificatesAsync(...)`. Code that reads it before that call gets `null`. The required ordering is: -> + > 1. Construct `new ApplicationInstance(telemetry)`. > 2. Load `ApplicationConfiguration` (e.g. via `LoadApplicationConfigurationAsync`). > 3. `await applicationInstance.CheckApplicationInstanceCertificatesAsync(silent: false, ..., ct);`. @@ -300,8 +324,10 @@ functional forwarders to the new design for binary-compatibility, but emit `CS06 #### Migrating the `CertificateValidator.CertificateValidation` event -The legacy event with mutable `e.Accept = true` mutability has been replaced by -the structured `CertificateValidationOptions.AcceptError` callback: +The legacy event with mutable `e.Accept = true` has been replaced by +the structured `CertificateValidationOptions.AcceptError` callback. The following +untrusted-certificate exception is for controlled development only; configure +trust stores instead in production. ```csharp // Before: @@ -321,7 +347,7 @@ var options = new CertificateValidationOptions error.StatusCode == StatusCodes.BadCertificateUntrusted }; CertificateValidationResult result = - await applicationInstance.CertificateManager.ValidateAsync(cert, options: options); + await configuration.CertificateManager.ValidateAsync(cert, options: options); if (!result.IsValid) { throw new ServiceResultException(result.StatusCode); @@ -333,17 +359,16 @@ if (!result.IsValid) `CertificateValidator.ValidateApplicationUri(...)` and `CertificateValidator.ValidateDomains(...)` are now exposed as extension methods on `ICertificateValidatorEx` in the -`Opc.Ua.CertificateValidationExtensions` static class. Existing call sites -that previously used the legacy class continue to work transparently. +`Opc.Ua.CertificateValidationExtensions` static class. Change the receiver to +`configuration.CertificateManager`; the removed legacy class is not a +compatibility shim. > The `CertificateFactory.DefaultKeySize` / `DefaultLifeTime` / `DefaultHashSize` constants are > intentionally **not** marked obsolete; they remain the canonical default values used across > configuration sites. -To suppress `CS0618` warnings while migrating, add at the top of affected files: -```csharp -#pragma warning disable CS0618 // Obsolete API usage during migration -``` +Replace the obsolete calls rather than suppressing `CS0618` across a file. +Warnings for forwarders do not imply that the removed APIs above are available. --- diff --git a/docs/migrate/2.0.x/configuration.md b/docs/migrate/2.0.x/configuration.md index 8fe12d61bb..ee2850c5e5 100644 --- a/docs/migrate/2.0.x/configuration.md +++ b/docs/migrate/2.0.x/configuration.md @@ -2,6 +2,18 @@ > **When to read this:** Read this for `ApplicationConfiguration` changes, the removed Data-Contract serializer, Newtonsoft removal from `Opc.Ua.Core`, the new `ParseExtension` / `UpdateExtension` signature, and session / browser state persistence. +## Contents + +- [Configuration](#configuration) + - [Data Contract Serializer support removed](#data-contract-serializer-support-removed) + - [TraceConfiguration apply APIs removed](#traceconfiguration-apply-apis-removed) + - [MinMetadataSamplingInterval removed, MinSupportedSamplingInterval added](#minmetadatasamplinginterval-removed-minsupportedsamplinginterval-added) + - [Newtonsoft.Json removed from Opc.Ua.Core](#newtonsoftjson-removed-from-opcuacore) + - [ParseExtension/UpdateExtension signature changed](#parseextensionupdateextension-signature-changed) + - [ExtensionObject array helpers changed](#extensionobject-array-helpers-changed) + - [IJsonEncodeable interface removed](#ijsonencodeable-interface-removed) +- [Session and Browser State Persistence](#session-and-browser-state-persistence) + ## Configuration ### Data Contract Serializer support removed diff --git a/docs/migrate/2.0.x/encoders.md b/docs/migrate/2.0.x/encoders.md index 0cf1984c70..77147ce27b 100644 --- a/docs/migrate/2.0.x/encoders.md +++ b/docs/migrate/2.0.x/encoders.md @@ -1,6 +1,18 @@ # Encoders and Complex Types -> **When to read this:** Read this for `IEncodeableFactoryBuilder` / `IType` / `EncodeableFactory.GlobalFactory` migration, JSON / XML / binary encoder/decoder changes (removed Default JSON encoding infrastructure, `IJsonEncodeable` removal), and complex-types moves to `Opc.Ua.Client`. +> **When to read this:** Read this for `IEncodeableFactoryBuilder` / `IType` / `EncodeableFactory.GlobalFactory` migration, JSON / XML / binary encoder/decoder changes (removed Default JSON encoding infrastructure, `IJsonEncodeable` removal), and the shared complex-type system in `Opc.Ua.Core.Schema` and its client-side factory helpers. + +## Contents + +- [Encodeable Factory and Complex Type System](#encodeable-factory-and-complex-type-system) + - [IType hierarchy](#itype-hierarchy) + - [IEncodeableTypeLookup changes](#iencodeabletypelookup-changes) + - [IEncodeableFactoryBuilder changes](#iencodeablefactorybuilder-changes) + - [EncodeableFactory.GlobalFactory removed](#encodeablefactoryglobalfactory-removed) +- [Encoders and Decoders](#encoders-and-decoders) +- [Complex Types](#complex-types) + - [ComplexTypes moved to Opc.Ua.Core.Schema](#complextypes-moved-to-opcuacoreschema) + - [OptionSet DataType support](#optionset-datatype-support) ## Encodeable Factory and Complex Type System @@ -26,22 +38,8 @@ New type abstraction layer: `IType` (base) with `IBuiltInType`, `IEnumeratedType The `[Obsolete]` static `EncodeableFactory.GlobalFactory` was removed. `EncodeableFactory.Create()` renamed to `Fork()`. Use `ServiceMessageContext.Factory` instead. -### ComplexTypes moved to Opc.Ua.Core.Schema - -The shared `ComplexTypeSystem` orchestrator, the complex type interfaces and the default (non-reflection-emit) type builder moved to the `Opc.Ua.Core.Schema` assembly under the root `Opc.Ua` namespace (which consumers already import) so they can be used by both client and server and existing code keeps compiling without adding a new `using`. Remove the old `Opc.Ua.Client.ComplexTypes` import if it is now unused (the client-only `NodeCacheResolver` and the `ComplexTypeSystem.Create(session, ...)` helpers stay in `Opc.Ua.Client.ComplexTypes`). -The `ComplexTypeSystem(ISession, ...)` constructors were removed; construct a session-bound instance with `ComplexTypeSystem.Create(session, telemetry)` (the default, NativeAOT friendly builder) or `ComplexTypeSystem.Create(session, new ComplexTypeBuilderFactory(), telemetry)` for the Reflection.Emit builder. `ComplexTypeSystem` is now `IDisposable`: an instance from `Create` (or from `IComplexTypeSystemFactory.Create`) owns the `NodeCacheResolver` and its node cache, so dispose it once no more types are loaded through it; loaded types stay registered. -Servers build the same stand-ins for runtime-loaded DataTypes **by default** (`StandardServer.LoadComplexTypes`; opt out by setting it to `false`); configure the pass with `AddComplexTypeSystem()` or invoke `IServerInternal.LoadComplexTypesAsync(...)` directly. See `docs/ComplexTypes.md`. - -### OptionSet DataType support - -Concrete Structure-backed sub-types of the abstract `OptionSet` DataType (`i=12755`) are now automatically registered by the default `ComplexTypeSystem` builder with a new runtime class `Opc.Ua.Encoders.OptionSet` (in `src/Opc.Ua.Types`). Bit-field metadata is resolved from `DataTypeDefinition` (`EnumDefinition`) or, as a fallback, synthesized from the `OptionSetValues` property (`LocalizedText[]`). - -Impact on existing code: - -- **Source-breaking for custom `IComplexTypeBuilder` implementations**: a new member `AddOptionSetType(QualifiedName, ExpandedNodeId, ExpandedNodeId, ExpandedNodeId, ExpandedNodeId, EnumDefinition)` was added to `IComplexTypeBuilder`. Custom implementations must provide it. -- The Reflection.Emit builder in `Opc.Ua.Client.ComplexTypes` throws `NotSupportedException` from `AddOptionSetType`; callers relying on the Reflection.Emit path for OptionSet sub-types should switch to the default builder (`ComplexTypeSystem.Create(session, telemetry)`). -- No wire-format changes: encoders/decoders continue to route through `IEncodeableFactory` → `IEncodeableType.CreateInstance`, which now yields `Opc.Ua.Encoders.OptionSet` for registered sub-types. -- UInteger-backed OptionSet DataTypes remain treated as their underlying unsigned integer in a `Variant` (unchanged). +For session-bound loading, ownership, and OptionSet changes, see +[Complex Types](#complex-types) below. ## Encoders and Decoders @@ -74,7 +72,7 @@ behaviour is unchanged. Implement `IEncoder` directly instead of deriving from a The shared `ComplexTypeSystem` orchestrator, the complex type interfaces and the default (non-reflection-emit) type builder moved to the `Opc.Ua.Core.Schema` assembly under the root `Opc.Ua` namespace (which consumers already import) so they can be used by both client and server and existing code keeps compiling without adding a new `using`. Remove the old `Opc.Ua.Client.ComplexTypes` import if it is now unused (the client-only `NodeCacheResolver` and the `ComplexTypeSystem.Create(session, ...)` helpers stay in `Opc.Ua.Client.ComplexTypes`). The `ComplexTypeSystem(ISession, ...)` constructors were removed; construct a session-bound instance with `ComplexTypeSystem.Create(session, telemetry)` (the default, NativeAOT friendly builder) or `ComplexTypeSystem.Create(session, new ComplexTypeBuilderFactory(), telemetry)` for the Reflection.Emit builder. `ComplexTypeSystem` is now `IDisposable`: an instance from `Create` (or from `IComplexTypeSystemFactory.Create`) owns the `NodeCacheResolver` and its node cache, so dispose it once no more types are loaded through it; loaded types stay registered. -Servers build the same stand-ins for runtime-loaded DataTypes **by default** (`StandardServer.LoadComplexTypes`; opt out by setting it to `false`); configure the pass with `AddComplexTypeSystem()` or invoke `IServerInternal.LoadComplexTypesAsync(...)` directly. See `docs/ComplexTypes.md`. +Servers build the same stand-ins for runtime-loaded DataTypes **by default** (`StandardServer.LoadComplexTypes`; opt out by setting it to `false`). Configure complex-type loading with `AddComplexTypeSystem()`, or load the types directly with `IServerInternal.LoadComplexTypesAsync(...)`. See [Complex Types](../../ComplexTypes.md). ### OptionSet DataType support @@ -94,4 +92,3 @@ Impact on existing code: - Related: [types.md](types.md), [source-generation.md](source-generation.md). - [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table. - [Migration Guide](../../MigrationGuide.md) — landing page across versions. - diff --git a/docs/migrate/2.0.x/identity.md b/docs/migrate/2.0.x/identity.md index 67f75098ce..d9bacdb24f 100644 --- a/docs/migrate/2.0.x/identity.md +++ b/docs/migrate/2.0.x/identity.md @@ -2,104 +2,67 @@ > **When to read this:** Read this for the pluggable identity-provider model (`IClientIdentityProvider`, `IUserTokenAuthenticator`, `IAccessTokenProvider`, `ITokenIssuer`, `IIdentityClaims`), the new `IUserIdentityTokenHandler` registry, and the secret-store / caller-password registry. +## Contents + +- [User Identity Token Handlers](#user-identity-token-handlers) + - [Migrating from 1.5.x](#migrating-from-15x) + - [Earlier 2.0 previews](#earlier-20-previews) +- [User Identity Providers](#user-identity-providers) + - [`SessionManager.ImpersonateUser` → registry authenticators](#sessionmanagerimpersonateuser--registry-authenticators) + - [`ManagedSessionOptions.Identity` → `IdentityProvider`](#managedsessionoptionsidentity--identityprovider) +- [Secrets — caller-supplied passwords go through a secret registry](#secrets--caller-supplied-passwords-go-through-a-secret-registry) + ## User Identity Token Handlers -**Breaking Change**: Identity tokens no longer perform cryptographic -operations directly. The handler pattern introduced earlier is now -**fully asynchronous** and **non-disposable**, and the -`Certificate`-taking ctors of `UserIdentity` and -`X509IdentityTokenHandler` have been removed in favour of a -`CertificateIdentifier` + `ICertificateProvider` model that resolves -the private-key cert on demand. +### Migrating from 1.5.x -**Before**: +The wire token types are unchanged, but cryptographic operations now live on +`IUserIdentityTokenHandler` rather than on the generated identity tokens. +Most applications should use [identity providers](../../IdentityProviders.md) +and let session activation handle the token cryptography. + +For X.509 identities, replace direct certificate-based construction with an +identifier and the certificate manager's provider. Initialize the application +certificates before accessing `configuration.CertificateManager`: ```csharp - var token = new X509IdentityToken(); - using var handler = token.AsTokenHandler(); - handler.Encrypt(certificate, nonce, securityPolicy, context); - handler.Decrypt(certificate, nonce, securityPolicy, context); - var signature = handler.Sign(data, securityPolicy); - bool isValid = handler.Verify(data, signature, securityPolicy); - - using var userIdentity = new UserIdentity(certificate); // legacy ctor +UserIdentity userIdentity = await UserIdentity.CreateAsync( + certificateIdentifier, + passwordProvider, + configuration.CertificateManager.CertificateProvider, + ct); ``` -**After**: +For code that operates on raw tokens, obtain a handler with `token.AsTokenHandler()` +and await `EncryptAsync`, `DecryptAsync`, `SignAsync`, or `VerifyAsync`. Handlers +are not disposable. The current signatures, including optional certificate and +ephemeral-key arguments, are defined by +[`IUserIdentityTokenHandler`](../../../src/Opc.Ua.Core/Stack/Types/IUserIdentityTokenHandler.cs). +Do not replace the asynchronous operations with blocking waits. -```csharp - var token = new X509IdentityToken(); - var handler = token.AsTokenHandler(); // not IDisposable - await handler.EncryptAsync(certificate, nonce, securityPolicy, context, ct: ct); - await handler.DecryptAsync(certificate, nonce, securityPolicy, context, ct: ct); - SignatureData signature = await handler.SignAsync(data, securityPolicy, ct); - bool isValid = await handler.VerifyAsync(data, signature, securityPolicy, ct); +### Earlier 2.0 previews - // New cert-based UserIdentity: identifier + cache-aware provider. - UserIdentity userIdentity = await UserIdentity.CreateAsync( - certificateIdentifier, - passwordProvider, - configuration.CertificateManager.CertificateProvider, - ct); -``` +The following table applies only to applications that adopted earlier preview +handler/provider APIs. These are not additional APIs that 1.5.x consumers must +have used. -**New interface shape**: +| Earlier preview API | Current replacement | +| --- | --- | +| `IUserIdentityTokenHandler : IDisposable` | Drop `using` on handlers; the current interface is not disposable. | +| `UserIdentity : IDisposable` | Drop `using` on identities; manage certificate and secret ownership through their providers. | +| Synchronous `handler.Encrypt`, `Decrypt`, `Sign`, `Verify` | Await the corresponding `*Async` operation. | +| `new UserIdentity(Certificate)` | Use `UserIdentity.CreateAsync(certificateIdentifier, passwordProvider, certificateProvider, ct)`. | +| `new X509IdentityTokenHandler(Certificate)` | Use the `CertificateIdentifier`, `ICertificatePasswordProvider`, and `ICertificateProvider` constructor. | +| `UserIdentity.CreateAsync(certId, passwordProvider, telemetry, ct)` | Pass an `ICertificateProvider` instead of the telemetry argument. | -```csharp - public interface IUserIdentityTokenHandler : - ICloneable, IEquatable - { - UserIdentityToken Token { get; } - string DisplayName { get; } - UserTokenType TokenType { get; } +The current handler types are `AnonymousIdentityTokenHandler`, +`UserNameIdentityTokenHandler`, `X509IdentityTokenHandler`, and +`IssuedIdentityTokenHandler`. - void UpdatePolicy(UserTokenPolicy userTokenPolicy); - - ValueTask EncryptAsync( - Certificate receiverCertificate, byte[] receiverNonce, - string securityPolicyUri, IServiceMessageContext context, - ..., CancellationToken ct = default); - ValueTask DecryptAsync( - Certificate certificate, Nonce receiverNonce, - string securityPolicyUri, IServiceMessageContext context, - ..., CancellationToken ct = default); - ValueTask SignAsync( - byte[] dataToSign, string securityPolicyUri, - CancellationToken ct = default); - ValueTask VerifyAsync( - byte[] dataToVerify, SignatureData signatureData, - string securityPolicyUri, CancellationToken ct = default); - } -``` - -**Migration required**: - -| Removed | Replacement | -| ------- | ----------- | -| `IUserIdentityTokenHandler : IDisposable` | `IUserIdentityTokenHandler` (no `IDisposable`). Drop `using` on handler instances. Sensitive byte buffers (`UserNameIdentityTokenHandler.DecryptedPassword`, `IssuedIdentityTokenHandler.DecryptedTokenData`) are no longer cleared on disposal — secure-memory management is the secret store's responsibility (deferred to a future revision). | -| `UserIdentity : IDisposable`, `UserIdentity.Dispose()` | `UserIdentity` (no `IDisposable`). Drop `using` on `new UserIdentity(...)`. | -| `handler.Encrypt(...)` (sync) | `await handler.EncryptAsync(..., ct)` | -| `handler.Decrypt(...)` (sync) | `await handler.DecryptAsync(..., ct)` | -| `SignatureData handler.Sign(...)` (sync) | `await handler.SignAsync(..., ct)` | -| `bool handler.Verify(...)` (sync) | `await handler.VerifyAsync(..., ct)` | -| `new UserIdentity(Certificate)` (legacy ctor) | `await UserIdentity.CreateAsync(certificateIdentifier, passwordProvider, certificateProvider, ct)` — the new ctor stores the identifier; the cert is materialised on demand by the provider. | -| `new X509IdentityTokenHandler(Certificate)` | `new X509IdentityTokenHandler(CertificateIdentifier, ICertificatePasswordProvider, ICertificateProvider)` — handler holds no live Certificate; on `SignAsync` the provider's cache is consulted (`TryGetPrivateKeyCertificate`) then the store (`GetPrivateKeyCertificateAsync`). | -| `[Obsolete] new UserIdentity(CertificateIdentifier, CertificatePasswordProvider)` | `await UserIdentity.CreateAsync(certificateIdentifier, passwordProvider, certificateProvider, ct)` — the obsolete ctor blocked on async; the new factory does not pre-resolve. | -| `await UserIdentity.CreateAsync(certId, passwordProvider, telemetry, ct)` | `await UserIdentity.CreateAsync(certId, passwordProvider, certificateProvider, ct)` — `ICertificateProvider` (typically `configuration.CertificateManager.CertificateProvider`) replaces the telemetry-only argument list. | - -**Available token handlers** (all non-disposable): - - `AnonymousIdentityTokenHandler` - - `UserNameIdentityTokenHandler` - - `X509IdentityTokenHandler` - - `IssuedIdentityTokenHandler` - -**Note on secure-memory management**: with `IDisposable` gone, the -sync `Array.Clear` of decrypted password / issued-token bytes that -used to happen in `Dispose()` no longer fires. Bytes live in plain -fields until GC. A follow-up revision will route inbound decrypted -secrets through the new `ISecretStore` abstraction (see *Secrets* -below) so secure clearing becomes the store's responsibility, with no -public surface change. +**Sensitive-buffer lifetime:** handlers provide no disposal hook for clearing +server-side decrypted password or issued-token buffers. Do not log or retain +those buffers. The secret registry below manages caller-supplied secrets; it +does not promise secure clearing of every inbound decrypted token buffer. ## User Identity Providers diff --git a/docs/migrate/2.0.x/node-states.md b/docs/migrate/2.0.x/node-states.md index d16ccdb06b..fca7771e09 100644 --- a/docs/migrate/2.0.x/node-states.md +++ b/docs/migrate/2.0.x/node-states.md @@ -2,6 +2,17 @@ > **When to read this:** Read this when migrating custom NodeManagers, `NodeState` clone / read / write helpers (`Clone` -> `CreateCopy`, removed `BaseVariableState` helpers), the new `INodeManager3` role-permission hooks, `OnAfterCreate(CancellationToken)`, predefined-node processing, generics on `BaseVariableState` / `BaseVariableTypeState`, code that took `lock (node)` on a `NodeState` or used `NodeBrowser.DataLock`, or `INodeCache.InvalidateNode`. +## Contents + +- [Node States](#node-states) + - [Generics and Typed BaseVariableState and BaseVariableTypeState](#generics-and-typed-basevariablestate-and-basevariabletypestate) + - [Predefined node processing](#predefined-node-processing) + - [NodeState Cloning and Lifecycle](#nodestate-cloning-and-lifecycle) + - [INodeManager3 - new role-permission and method-resolution hooks](#inodemanager3---new-role-permission-and-method-resolution-hooks) + - [NodeState guards itself; NodeBrowser is single-consumer (UA0027)](#nodestate-guards-itself-nodebrowser-is-single-consumer-ua0027) + - [NodeBrowser gains an async iteration seam](#nodebrowser-gains-an-async-iteration-seam) +- [`INodeCache` changes](#inodecache-changes) + ## Node States ### Generics and Typed BaseVariableState and BaseVariableTypeState @@ -10,7 +21,7 @@ With the changes to Variant, the generic node state classes reflecting the inner 1. T is a built in type -> use `VariantBuilder` 2. T is a instance of `IEncodeable` (a complex structure) -> Use `StructureBuilder` where T is the name of the structure. -3. T is an instance of Enum (an enumeration) -> Use `EnumBuilder` where T is the name fo the enumeration type. +3. T is an instance of Enum (an enumeration) -> Use `EnumBuilder` where T is the name of the enumeration type. E.g. to create an instance of a `PropertyState` where T is `ArrayOf` use @@ -391,7 +402,7 @@ Version 2.0 collapses the two parallel node-cache contracts into a single public | `FetchSuperTypesAsync(ExpandedNodeId, ct)` | extension method that loops `FindSuperTypeAsync`. | | `GetNodeWithBrowsePathAsync(NodeId, ArrayOf, ct)` | extension method on `NodeCacheExtensions`. | | `GetBuiltInTypeAsync(NodeId, ct)` | extension method on `NodeCacheExtensions`. | - | `GetDisplayTextAsync(INode | ExpandedNodeId | ReferenceDescription, ct)` | three extension methods on `NodeCacheExtensions`. | + | `GetDisplayTextAsync(INode \| ExpandedNodeId \| ReferenceDescription, ct)` | three extension methods on `NodeCacheExtensions`. | External implementations of `INodeCache` no longer need to implement these members. Call sites that already used `using Opc.Ua;` keep compiling unchanged because the extensions live in the same namespace. diff --git a/docs/migrate/2.0.x/packages.md b/docs/migrate/2.0.x/packages.md index b2a8ebe33e..28327457fc 100644 --- a/docs/migrate/2.0.x/packages.md +++ b/docs/migrate/2.0.x/packages.md @@ -2,7 +2,16 @@ > **When to read this:** Read this for NuGet package renames / additions / removals, the new published packages, target-framework changes on `Opc.Ua.Types`, and the Newtonsoft.Json removal from `Opc.Ua.Core`. -### New published packages +## Contents + +- [New published packages](#new-published-packages) +- [Renamed packages — the GDS libraries drop `.Common`](#renamed-packages--the-gds-libraries-drop-common) +- [Target Frameworks (only Opc.Ua.Types changes)](#target-frameworks-only-opcuatypes-changes) +- [NuGet dependency additions and removals](#nuget-dependency-additions-and-removals) +- [ASP.NET Core packages are versioned per target framework](#aspnet-core-packages-are-versioned-per-target-framework) +- [Newtonsoft.Json - what really changed](#newtonsoftjson---what-really-changed) + +## New published packages The 2.0 packages are currently prereleases on nuget.org. Use `2.0.0-preview.*` to float to the latest published `2.0.0-preview.N` release, @@ -23,7 +32,7 @@ Two assemblies that previously shipped only as transitive content inside `Opc.Ua ``` -### Renamed packages — the GDS libraries drop `.Common` +## Renamed packages — the GDS libraries drop `.Common` The three GDS packages lose their `.Common` suffix, so the family matches every other companion specification in the stack (`Opc.Ua.Di`, `Opc.Ua.ISA95`, @@ -55,13 +64,20 @@ Assembly names follow the package ids (`Opc.Ua.Gds.Server.dll`, not `Opc.Ua.Gds.Server.Common.dll`), so update any binding redirects, ILMerge or trimming descriptors, or signing manifests that name the files directly. -### Target Frameworks (only Opc.Ua.Types changes) +## Target Frameworks (only Opc.Ua.Types changes) -The TFM matrix for the main libraries (Core, Client, Server, Configuration, etc.) is unchanged from 1.5.378: `net472;net48;netstandard2.1;net8.0;net9.0;net10.0`. The only consumer-visible change is the `Opc.Ua.Types` assembly: on 1.5.378 it tracked the dedicated `LibTypesTargetFrameworks` variable (`net472;net48;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0`); on 2.0 the variable is removed and `Opc.Ua.Types` tracks `LibCoreTargetFrameworks`, the same matrix as every other library. The net effect is that `netstandard2.0` is no longer offered for `Opc.Ua.Types`. +The main library target matrix includes `net472`, `net48`, `netstandard2.1`, +`net8.0`, `net9.0`, and `net10.0`. `Opc.Ua.Types` additionally targets +`netstandard2.0`: its project uses `$(LibCoreTargetFrameworks);netstandard2.0`. +A Types-only consumer does not need to retarget merely because it uses +`netstandard2.0`. Higher-level packages can have different requirements; check +the target assets of every package your application consumes. -The minimum SDK is the **.NET 10 SDK**, and projects compile with **`LangVersion 14.0`**. Projects that target `netstandard2.0` and pull in `Opc.Ua.Types` will fail to restore with `NU1202` ("package is not compatible") - retarget to `netstandard2.1` or one of the .NET / .NET Framework TFMs above. +Building this repository requires the **.NET 10 SDK** and **C# 14.0**. +Those build prerequisites are not the minimum runtime for every NuGet consumer. +See [platform support](../../DeveloperGuide.md#packages-platform-support-and-versioning). -### NuGet dependency additions and removals +## NuGet dependency additions and removals | Package | Status in 2.0 | Referenced by | |---|---|---| @@ -88,18 +104,18 @@ The minimum SDK is the **.NET 10 SDK**, and projects compile with **`LangVersion | `System.Reflection.Metadata` 9.0.0 | Added (pinned) | Centralised pin only, no direct reference; tracks `$(RoslynRuntimeVersion)` for the analyzer closure | | `Mono.Options` 6.12.0.148 | Removed | Previously referenced by `samples/Reference/ConsoleReferenceServer/MonoReferenceServer.csproj` | -### ASP.NET Core packages are versioned per target framework +## ASP.NET Core packages are versioned per target framework `Microsoft.AspNetCore.Authentication.Certificate`, `Microsoft.AspNetCore.Authentication.JwtBearer`, `Microsoft.AspNetCore.Mvc.Testing` and `Microsoft.AspNetCore.TestHost` ship one band per .NET major and, unlike the `Microsoft.Extensions.*` packages, carry no `netstandard2.0` asset and do not roll forward across majors - a `net8.0` project cannot consume the `10.0.x` band. `Directory.Packages.props` therefore selects the version from `$(TargetFramework)`: `net8.0` gets `8.0.30`, `net9.0` gets `9.0.19`, and every other TFM (including `net10.0` and the `net10.0` shell that legacy `netstandard2.0`/`netstandard2.1` `$(CustomTestTarget)` builds fall back to) gets `10.0.11`. Consumers that pin these packages themselves are unaffected. Consumers that inherit them transitively through `Opc.Ua.Bindings.Https` receive the band matching their own target framework. -### Newtonsoft.Json - what really changed +## Newtonsoft.Json - what really changed -`Newtonsoft.Json` was removed as a direct dependency of `src/Opc.Ua.Core/Opc.Ua.Core.csproj` in 2.0. The only direct `` remaining anywhere under `src/` and `src/` is in `src/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj`. Consequences: - -- Consumers that reached `Newtonsoft.Json` only transitively through `Opc.Ua.Core` now need to add their own explicit reference. -- Consumers of `Opc.Ua.PubSub` continue to receive `Newtonsoft.Json` transitively and are unaffected. +Neither `Opc.Ua.Core` nor `Opc.Ua.PubSub` directly references `Newtonsoft.Json` +in 2.0. Consumers that used it only through either package's old dependency +chain must add an explicit reference if their own code still uses its API. +Do not depend on an unrelated package to provide it transitively. ```xml diff --git a/docs/migrate/2.0.x/pubsub.md b/docs/migrate/2.0.x/pubsub.md index 91807909ee..714c708990 100644 --- a/docs/migrate/2.0.x/pubsub.md +++ b/docs/migrate/2.0.x/pubsub.md @@ -11,12 +11,13 @@ required for existing consumers. ## Contents -1. [PubSub assemblies and NuGet packages renamed and split](#1-pubsub-assemblies-and-nuget-packages-renamed-and-split) -2. [`UaPubSubApplication.Create*` and the legacy 1.04 API are replaced](#2-uapubsubapplicationcreate-and-the-legacy-104-api-are-replaced) -3. [JSON encoder switched to System.Text.Json](#3-json-encoder-switched-to-systemtextjson) -4. [`JsonEncodingMode` Reversible/Non-Reversible encodings removed](#4-jsonencodingmode-reversiblenon-reversible-encodings-removed) -5. [UADP RawData bounded-field wire-compatibility break](#5-uadp-rawdata-bounded-field-wire-compatibility-break) -6. [Compatibility matrix](#6-compatibility-matrix) +- [1. PubSub assemblies and NuGet packages renamed and split](#1-pubsub-assemblies-and-nuget-packages-renamed-and-split) +- [2. `UaPubSubApplication.Create*` and the legacy 1.04 API are replaced](#2-uapubsubapplicationcreate-and-the-legacy-104-api-are-replaced) +- [3. JSON encoder switched to System.Text.Json](#3-json-encoder-switched-to-systemtextjson) +- [4. `JsonEncodingMode` Reversible/Non-Reversible encodings removed](#4-jsonencodingmode-reversiblenon-reversible-encodings-removed) +- [5. UADP RawData bounded-field wire-compatibility break](#5-uadp-rawdata-bounded-field-wire-compatibility-break) +- [6. Compatibility matrix](#6-compatibility-matrix) +- [See also](#see-also) ## 1. PubSub assemblies and NuGet packages renamed and split @@ -94,8 +95,9 @@ replaced with a `System.Text.Json`-backed encoder under `src/Opc.Ua.PubSub/Encoding/Json/`. Behaviour changes that may surface in callers: -- The `Newtonsoft.Json` dependency is dropped from the PubSub layer (it remains - transitively available via `Opc.Ua.Core` for legacy Variant JSON). +- The `Newtonsoft.Json` dependency is dropped. Core does not supply it either; + add an explicit reference if application code still uses Newtonsoft APIs. See + [package dependency migration](packages.md#newtonsoftjson---what-really-changed). - Numeric round-trips honour the .NET native precision instead of the Newtonsoft default (e.g. `double` → 17 significant digits, not 15). - The new encoder is `Utf8JsonWriter`-backed; allocations on the hot path drop diff --git a/docs/migrate/2.0.x/sessions-subscriptions.md b/docs/migrate/2.0.x/sessions-subscriptions.md index f05622b5cb..cf9531ad93 100644 --- a/docs/migrate/2.0.x/sessions-subscriptions.md +++ b/docs/migrate/2.0.x/sessions-subscriptions.md @@ -6,6 +6,27 @@ > subscriptions, removed `ReverseConnectClientCollection`, and transport > abstraction changes. +## Contents + +- [GDS Client API modernization](#gds-client-api-modernization) + - [`Task` → `ValueTask` on GDS client interfaces](#task--valuetask-on-gds-client-interfaces) + - [Removal of obsolete GDS APIs](#removal-of-obsolete-gds-apis) +- [ManagedSession and Automatic Reconnection](#managedsession-and-automatic-reconnection) + - [Configuring Reconnection Policy](#configuring-reconnection-policy) + - [Server Redundancy](#server-redundancy) + - [Service Call Behavior During Reconnect](#service-call-behavior-during-reconnect) + - [Fluent Builder, V2 Subscriptions, and Dependency Injection](#fluent-builder-v2-subscriptions-and-dependency-injection) +- [Server Session Activation and Subscription Transfer](#server-session-activation-and-subscription-transfer) +- [Subscriptions and Transports](#subscriptions-and-transports) + - [Durable subscriptions and reshaped Subscription tree](#durable-subscriptions-and-reshaped-subscription-tree) + - [Request completion is owned by `OperationContext`](#request-completion-is-owned-by-operationcontext) + - [`Opc.Ua.Server.ISession`: `IsClosing` and `InvalidateContinuationPoints`](#opcuaserverisession-isclosing-and-invalidatecontinuationpoints) + - [`Opc.Ua.Server.ISubscription`: the publish pipeline is server-internal](#opcuaserverisubscription-the-publish-pipeline-is-server-internal) + - [PubSub](#pubsub) + - [Reverse connect](#reverse-connect) + - [`IMessageSocket` abstraction removed](#imessagesocket-abstraction-removed) + - [Transport binding registry — `TransportBindings` static API removed](#transport-binding-registry--transportbindings-static-api-removed) + ## GDS Client API modernization The `Opc.Ua.Gds.Client` package has undergone a significant cleanup. Two breaking changes affect almost every consumer of the GDS / LDS / Server-Push client APIs. @@ -202,14 +223,14 @@ ManagedSession session = await new ManagedSessionBuilder(configuration, telemetr The V2 options-based subscription manager (`ISubscriptionManager`) is exposed on `ISession` through `bool TryGetSubscriptionManager(out ISubscriptionManager? manager)` — it returns `true` and the manager for V2-engine sessions (the default for `ManagedSession`) and `false` for classic-engine sessions. The classic `Subscriptions` property remains available alongside it. Use `UseSubscriptionEngine(ClassicSubscriptionEngineFactory.Instance)` on the builder if you need the legacy classic engine instead. > **Source-breaking (2.0 preview):** the earlier throwing `ManagedSession.SubscriptionManager` property has been **removed** in favor of `ISession.TryGetSubscriptionManager`. Replace `var manager = session.SubscriptionManager;` (which threw `InvalidOperationException` on classic-engine sessions) with: -> + > ```csharp > if (session.TryGetSubscriptionManager(out ISubscriptionManager? manager)) > { > // use manager (V2 engine) > } > ``` -> + > The `session.AddSubscription(...)` fluent extensions are unchanged. ```csharp @@ -393,8 +414,7 @@ already implements them. **Source-breaking for custom implementations and for callers.** This is the **server-side** `Opc.Ua.Server.ISubscription` — not the client-side `Opc.Ua.Client.Subscriptions.ISubscription` introduced by the V2 subscription shape above. Twelve members leave the interface -(analyzer `UA0030`; see [MigrationGuide.md](../../MigrationGuide.md#ua0030) for the -caller-side migration): +(analyzer `UA0030`): - `ItemReadyToPublish` and `ItemNotificationsAvailable` are **deleted**. Their implementation bodies had been commented out since 1.5.x, so every call was a no-op — delete the calls and @@ -427,13 +447,22 @@ rest of the pipeline members. queue is the other half of the publish protocol. Code that constructed or drove one should use the `Publish` service path via `ISubscriptionManager.PublishAsync`. +For the public operations that remain, obtain a subscription with +`ISubscriptionManager.TryGetSubscription` before calling `ResendData`, +`GetMonitoredItems`, or the monitored-item operations. Acknowledgements belong +in the Publish request, not in direct calls into the subscription's internal +publishing state machine. + ### PubSub -**Not source-breaking.** No public top-level types in `Opc.Ua.PubSub` were removed or renamed in 2.0. Changes are limited to internal modernization, AOT preparation, and diagnostics improvements. `Newtonsoft.Json` remains a direct `` of `src/Opc.Ua.PubSub/Opc.Ua.PubSub.csproj`, so PubSub consumers keep receiving it transitively (see [Newtonsoft.Json - what really changed](#newtonsoftjson---what-really-changed)). +PubSub has source-breaking package and API changes. See the +[PubSub migration guide](pubsub.md) for removed application/connection types, +builder replacements, JSON encoding changes, and wire-compatibility limits. +Dependency changes are maintained in the [package guide](packages.md#newtonsoftjson---what-really-changed). ### Reverse connect -**Not source-breaking.** `ReverseConnectManager`, `ReverseConnectProperty`, and `ReverseConnectServer` retain the same public shape in 2.0. The previously published `ReverseConnectClientCollection` wrapper has been removed; this is already covered by the broader [Configuration collection types removed](#configuration-collection-types-removed) guidance. +`ReverseConnectManager`, `ReverseConnectProperty`, and `ReverseConnectServer` retain the same public shape in 2.0. The `ReverseConnectClientCollection` wrapper is removed, which is source-breaking for callers using that collection. See [Configuration collection types removed](types.md#configuration-collection-types-removed). ### `IMessageSocket` abstraction removed diff --git a/docs/migrate/2.0.x/source-generation.md b/docs/migrate/2.0.x/source-generation.md index 44c630db70..f439bf9a2e 100644 --- a/docs/migrate/2.0.x/source-generation.md +++ b/docs/migrate/2.0.x/source-generation.md @@ -2,7 +2,17 @@ > **When to read this:** Read this for the move from pre-generated code files to the source-generated NodeManager / data-type model, including the project-structure changes and the new default for boolean properties. -Instead of generating code for OPC UA design files using the [ModelCompiler](https://github.com/OPCFoundation/UA-ModelCompiler), this version of the stack uses [Source Generators](https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/#source-generators) to generate code behind for your project. Input into the source generator can be NodeSet2.xml files or ModelDesign.xml files (the same that ModelCompiler consumes). Example projects are provided in the Applications folder. Source generators are Roslyn analyzers, that are called by the Roslyn compiler and emit code during the build process. +## Contents + +- [Migrating model projects](#migrating-model-projects) +- [Default value of boolean properties in source-generated data types is now false](#default-value-of-boolean-properties-in-source-generated-data-types-is-now-false) +- [Server default Aggregate configuration now treats Uncertain as Bad (Part 13)](#server-default-aggregate-configuration-now-treats-uncertain-as-bad-part-13) +- [MIG01 resolution playbook](#mig01-resolution-playbook) +- [Project Structure](#project-structure) + +## Migrating model projects + +Instead of generating code for OPC UA design files using the [ModelCompiler](https://github.com/OPCFoundation/UA-ModelCompiler), this version of the stack uses [Source Generators](https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/#source-generators) to generate code behind for your project. Input into the source generator can be NodeSet2.xml files or ModelDesign.xml files (the same that ModelCompiler consumes). Example projects are provided under `samples/`. Source generators are Roslyn analyzers, that are called by the Roslyn compiler and emit code during the build process. **Model compiler generated csharp code is not supported in this version!** @@ -30,9 +40,9 @@ Code generation during compilation also allows not just emitting code ahead of t The stack itself uses source generators to generate the core opc ua code. Therefore all pre-generated code files (`Generated/` folders) have been removed and are now generated at build time. As a result of using source generators to generate the stack code all `*.nodeset2.xml` files previously included as embedded zip have been removed. Also, all `*.Types.xsd` and `*.Types.bsd` files are now included as string resource instead of embedded resources. If you need access to these, use the new `Schemas.XmlAsStream` and `Schemas.BinaryAsStream` APIs in the node manager namespace which produce a utf8 stream. Alternatively you can use the existing ModelCompiler tool to generate these files. -When you encounter slower build times use incremental compilation and avoid changes to code in Opc.Ua and Opc.Ua.Core project. In addition you can change your builds to only build for your target framework using the dotnet `-f ` command line option, e.g. `-f net10`. +When you encounter slower build times use incremental compilation and avoid changes to code in Opc.Ua and Opc.Ua.Core project. In addition you can change your builds to only build for your target framework using the dotnet `-f ` command line option, e.g. `-f net10.0`. -### Default value of boolean properties in source-generated data types is now false +## Default value of boolean properties in source-generated data types is now false **Breaking Change**: Boolean properties on source-generated data types now correctly default to `false` instead of `true`. @@ -70,7 +80,7 @@ var connection = new PubSubConnectionDataType }; ``` -### Server default Aggregate configuration now treats Uncertain as Bad (Part 13) +## Server default Aggregate configuration now treats Uncertain as Bad (Part 13) **Behavioral Change (Part 13 compliance)**: The server-side default aggregate configuration returned by `AggregateManager.GetDefaultConfiguration(...)` — used when a `ReadProcessedDetails` request sets @@ -83,7 +93,7 @@ Uncertain-quality samples as Bad when computing aggregate `StatusCode`s (unless definition states otherwise). Clients that require the previous behavior should send an explicit `AggregateConfiguration` with `TreatUncertainAsBad = false` instead of `UseServerCapabilitiesDefaults = true`. -### MIG01 resolution playbook +## MIG01 resolution playbook The migration analyzer's source generator emits temporary `Collection : List` compatibility wrappers. It reports `MIG01` when @@ -106,7 +116,7 @@ replacing the wrapper with the intended fully qualified `List` or wrapper explicitly in consumer source so the unresolved reference binds and the generator does not try to synthesize it. -### Project Structure +## Project Structure New `Opc.Ua` project as an intermediate project. Impact: diff --git a/docs/migrate/2.0.x/telemetry.md b/docs/migrate/2.0.x/telemetry.md index 3374f97a97..91e04b86ce 100644 --- a/docs/migrate/2.0.x/telemetry.md +++ b/docs/migrate/2.0.x/telemetry.md @@ -35,6 +35,29 @@ ISession session = await factory.CreateAsync(/* ... */); --- +## Contents + +- [Replacing the static logger surface](#replacing-the-static-logger-surface) + - [From static logger management](#from-static-logger-management) + - [From static `Utils.Log*` methods](#from-static-utilslog-methods) + - [From `Utils.Trace*` methods](#from-utilstrace-methods) +- [Non-exhaustive summary of breaking changes](#non-exhaustive-summary-of-breaking-changes) + - [Core infrastructure constructors](#core-infrastructure-constructors) + - [Interface additions](#interface-additions) + - [System context](#system-context) + - [Certificate and security](#certificate-and-security) + - [Transport layer](#transport-layer) + - [Client library](#client-library) + - [Configuration library](#configuration-library) + - [Server library](#server-library) + - [PubSub library](#pubsub-library) + - [Method signatures](#method-signatures) + - [Removed and `[Obsolete]` `Utils` static APIs](#removed-and-obsolete-utils-static-apis) +- [ETW `EventSource` provider removal](#etw-eventsource-provider-removal) + - [`ClientTraceFlags.EventLog` removed (source breaking)](#clienttraceflagseventlog-removed-source-breaking) + - [Migrating `EventListener` / `dotnet-trace` consumers](#migrating-eventlistener--dotnet-trace-consumers) +- [Migration utilities](#migration-utilities) + ## Replacing the static logger surface The legacy static logger management (`Utils.SetLogger` / @@ -108,9 +131,9 @@ explicit telemetry context**. ### Core infrastructure constructors -- `ServiceMessageContext()` → `ServiceMessageContext(ITelemetryContext)` +- `ServiceMessageContext()` → `ServiceMessageContext(ITelemetryContext)` (parameter-less constructor removed). -- `EncodeableFactory()` → `EncodeableFactory(ITelemetryContext)` +- `EncodeableFactory()` → `EncodeableFactory(ITelemetryContext)` (parameter-less constructor removed). - `ApplicationConfiguration` gains `ApplicationConfiguration(ITelemetryContext)`. @@ -128,7 +151,7 @@ explicit telemetry context**. ### System context -- `SystemContext()` → `SystemContext(ITelemetryContext)` and +- `SystemContext()` → `SystemContext(ITelemetryContext)` and `SystemContext(IOperationContext, ITelemetryContext)` (parameter-less constructor removed). @@ -137,9 +160,9 @@ explicit telemetry context**. - `CertificateValidator` gains `CertificateValidator(ITelemetryContext)`. - `CertificateStoreIdentifier.CreateStore(string, ITelemetryContext)` - and `OpenStore(ITelemetryContext)` — custom stores must + and `OpenStore(ITelemetryContext)` — custom stores must implement the new signature. -- `CertificateIdentifier.OpenStore(ITelemetryContext)` — same +- `CertificateIdentifier.OpenStore(ITelemetryContext)` — same custom-store impact. - `CertificateIdentifierCollection` no longer implements `ICertificateStore`; use `CertificateIdentifierCollectionStore` @@ -197,10 +220,10 @@ explicit telemetry context**. ### Method signatures - `UserIdentityToken.GetOrCreateCertificate(ITelemetryContext)` - — certificate creation is no longer implicit on the + — certificate creation is no longer implicit on the `Certificate` getter; call this method explicitly when needed. - `ServerSecurityPolicy.CalculateSecurityLevel(..., ILogger)` - — the non-logger overload is `[Obsolete]`. + — the non-logger overload is `[Obsolete]`. ### Removed and `[Obsolete]` `Utils` static APIs @@ -244,7 +267,7 @@ The stack shipped four internal `EventSource` providers for high-performance tra | `OPC-UA-Server` | `Opc.Ua.Server` | `OPC-UA-Server` | | `Opc.Ua.ChannelManager` | `Opc.Ua.Core` (client channel manager) | `Opc.Ua.ChannelManager` | -The replacement logger category is always the **exact old ETW provider name**, not the assembly's usual typed category. Migrated filters can therefore keep the same identifying string (for example, `AddFilter("OPC-UA-Client", LogLevel.Trace)`) after they move from an ETW provider subscription to `ILogger` configuration. `EventLevel` mapped to `LogLevel` on a like-for-like basis (`Verbose` → `Trace`, `Informational` → `Information`, `Warning` → `Warning`, `Error`/`Critical` → `Error`/`Critical`). See [DeveloperGuide.md — narrow exception: retained EventSource-compatibility ids](../../DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) for the authoring-side rules and [Sessions.md — diagnostics surface contract](../../Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) for the full `Opc.Ua.ChannelManager` event table. +The replacement logger category is always the **exact old ETW provider name**, not the assembly's usual typed category. Migrated filters can therefore keep the same identifying string (for example, `AddFilter("OPC-UA-Client", LogLevel.Trace)`) after they move from an ETW provider subscription to `ILogger` configuration. `EventLevel` mapped to `LogLevel` on a like-for-like basis (`Verbose` → `Trace`, `Informational` → `Information`, `Warning` → `Warning`, `Error`/`Critical` → `Error`/`Critical`). See [DeveloperGuide.md — narrow exception: retained EventSource-compatibility ids](../../DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) for the authoring-side rules and [Sessions.md — diagnostics surface contract](../../Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) for the full `Opc.Ua.ChannelManager` event table. The `OPC-UA-Server` compatibility category remains opt-in like the retired EventSource provider. Enable that category at `Trace` to receive its records, including the `ServerCall` and `SessionState` records whose retained legacy level is `Information`. A global `Information` minimum therefore does not emit a record for every server request. @@ -311,15 +334,15 @@ builder.Services.AddOpenTelemetry().WithLogging(l => l To aid migration the stack provides: -- `DefaultTelemetry.Create(...)` — convenient factory that +- `DefaultTelemetry.Create(...)` — convenient factory that returns an `ITelemetryContext` backed by the trace logger when no configuration is supplied. -- `Telemetry.NullLogger` / `Telemetry.NullLogger` — +- `Telemetry.NullLogger` / `Telemetry.NullLogger` — no-op-in-release / debug-check-in-debug logger you can assign to an `m_logger` field to avoid null-reference exceptions while a class is being migrated. Distinct from `NullLogger.Instance` in `Microsoft.Extensions.Logging.Abstractions`. -- `Utils.Fallback.Logger` — an `ILogger` that mimics the +- `Utils.Fallback.Logger` — an `ILogger` that mimics the legacy static `ILogger`. Use as a strictly temporary placeholder in places where no telemetry context can yet be plumbed through. The `Fallback` class is marked `Experimental` and may be removed @@ -334,10 +357,10 @@ test time. **See also** - Related: [packages.md](packages.md), [configuration.md](configuration.md). -- [`docs/Diagnostics.md`](../../Diagnostics.md) — full +- [`docs/Diagnostics.md`](../../Diagnostics.md) — full end-state usage and extensibility guidance for `ITelemetryContext` (custom contexts, OpenTelemetry wiring, metrics inventory). -- [`docs/Sessions.md` — diagnostics surface contract](../../Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) — full `Opc.Ua.ChannelManager` compatibility event table and safe-field policy. -- [`docs/DeveloperGuide.md` — narrow exception: retained EventSource-compatibility ids](../../DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) — authoring rules for the retained compatibility ids. -- [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table. -- [Migration Guide](../../MigrationGuide.md) — landing page across versions. +- [`docs/Sessions.md` — diagnostics surface contract](../../Sessions.md#diagnostics-surface-contract--what-tags-and-structured-log-fields-carry) — full `Opc.Ua.ChannelManager` compatibility event table and safe-field policy. +- [`docs/DeveloperGuide.md` — narrow exception: retained EventSource-compatibility ids](../../DeveloperGuide.md#narrow-exception-retained-eventsource-compatibility-ids) — authoring rules for the retained compatibility ids. +- [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table. +- [Migration Guide](../../MigrationGuide.md) — landing page across versions. diff --git a/docs/migrate/2.0.x/timeprovider.md b/docs/migrate/2.0.x/timeprovider.md index b6c7f135c0..54f3ff2a9f 100644 --- a/docs/migrate/2.0.x/timeprovider.md +++ b/docs/migrate/2.0.x/timeprovider.md @@ -2,6 +2,13 @@ > **When to read this:** Read this when adopting `System.TimeProvider` across the stack (replacing direct `DateTime.UtcNow`, `Timer`, and similar timing primitives in custom NodeManagers, durable subscriptions, and reconnect policies). +## Contents + +- [Adopting TimeProvider](#adopting-timeprovider) +- [Monotonic timestamps for duration calculations](#monotonic-timestamps-for-duration-calculations) + +## Adopting TimeProvider + **Not source-breaking.** The stack now uses [`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider) as its canonical clock and scheduler so that timeouts, intervals, keep-alive loops, @@ -99,7 +106,7 @@ The `Timer` field type changes from `System.Threading.Timer` to `ITimer` — bot implement `IDisposable` and the same `Change` / `Dispose` semantics; only the parameter types on `Change` differ (`TimeSpan` instead of `int`/`uint`/`long`). -### Monotonic timestamps for duration calculations +## Monotonic timestamps for duration calculations `TimeProvider.GetTimestamp()` returns a `long` monotonic timestamp that does not suffer from the 32-bit wraparound of `Environment.TickCount` / `HiResClock.TickCount` @@ -134,4 +141,3 @@ TimeSpan elapsed = m_timeProvider.GetElapsedTime(startTimestamp); - Related: [sessions-subscriptions.md](sessions-subscriptions.md), [node-states.md](node-states.md). - [2.0 migration index](README.md) — analyzer quick-start + symptom → sub-doc table. - [Migration Guide](../../MigrationGuide.md) — landing page across versions. - diff --git a/docs/migrate/2.0.x/transport-listener-async.md b/docs/migrate/2.0.x/transport-listener-async.md index 75fc2b0164..85bd3d6e1d 100644 --- a/docs/migrate/2.0.x/transport-listener-async.md +++ b/docs/migrate/2.0.x/transport-listener-async.md @@ -11,6 +11,23 @@ There is **no `[Obsolete]` shim** for this change; the sync methods are gone. Update every call site that previously invoked `listener.Open` / `listener.Close` / `listener.Dispose`. +## Contents + +- [What changed](#what-changed) + - [`ITransportListener`](#itransportlistener) + - [`ITransportListenerCertificateRotation`](#itransportlistenercertificaterotation) + - [`ITransportListenerFactory`](#itransportlistenerfactory) + - [`ServerBase`](#serverbase) + - [`ReverseConnectHost`](#reverseconnecthost) + - [`ReverseConnectManager.RegisterWaitingConnection`](#reverseconnectmanagerregisterwaitingconnection) + - [Reverse-connect configuration providers](#reverse-connect-configuration-providers) + - [Dependency-injection startup](#dependency-injection-startup) +- [Migration steps](#migration-steps) + - [Application / sample code](#application--sample-code) + - [Custom transport binding implementations](#custom-transport-binding-implementations) + - [Custom server subclass (`StandardServer` / `LdsServer` heir)](#custom-server-subclass-standardserver--ldsserver-heir) +- [Why](#why) + ## What changed ### `ITransportListener` diff --git a/docs/migrate/2.0.x/types.md b/docs/migrate/2.0.x/types.md index 77f317ac90..1d7f7ab721 100644 --- a/docs/migrate/2.0.x/types.md +++ b/docs/migrate/2.0.x/types.md @@ -2,7 +2,29 @@ > **When to read this:** Read this when hit by `CS0029` / `CS1503` / `CS0266` on `NodeId`, `Variant`, `DataValue`, `ExtensionObject`, `QualifiedName`, `LocalizedText`, `ArrayOf` / `MatrixOf`, `ByteString`, `StatusCode`, `XmlElement`, `EnumValue`, or by `[Obsolete]` warnings on built-in type APIs - covers every value-type and `Variant`-for-`object` migration. Maps to analyzer rules `UA0002`–`UA0008`, `UA0014`, and `UA0019`. -### Several built in types are now immutable value types +## Contents + +- [Several built in types are now immutable value types](#several-built-in-types-are-now-immutable-value-types) +- [ByteString](#bytestring) +- [ArrayOf and MatrixOf](#arrayof-and-matrixof) + - [Configuration collection types removed](#configuration-collection-types-removed) + - [Generated data type fields with ValueRank=OneOrMoreDimensions](#generated-data-type-fields-with-valuerankoneormoredimensions) +- [DateTimeUtc](#datetimeutc) +- [QualifiedName and LocalizedText](#qualifiedname-and-localizedtext) +- [StatusCode](#statuscode) +- [NodeId/ExpandedNodeId](#nodeidexpandednodeid) +- [Variant, DataValue and ExtensionObject](#variant-datavalue-and-extensionobject) + - [Deprecated boxing behavior](#deprecated-boxing-behavior) + - [Replacement of all use of System.Object in generated code and API](#replacement-of-all-use-of-systemobject-in-generated-code-and-api) +- [DataValue](#datavalue) +- [XmlElement](#xmlelement) +- [EnumValue to represent the enumeration built in type](#enumvalue-to-represent-the-enumeration-built-in-type) +- [ExtensionObject array helpers changed](#extensionobject-array-helpers-changed) +- [Other Data Types](#other-data-types) +- [Obsoleted APIs and replacements](#obsoleted-apis-and-replacements) +- [APIs permanently removed](#apis-permanently-removed) + +## Several built in types are now immutable value types The `Variant` and `TypeInfo`, `NodeId`, `ExpandedNodeId`, `ExtensionObject`, `LocalizedText` and `QualifiedName` are now `readonly struct`s. This is a large breaking change and affects existing usage: @@ -10,7 +32,7 @@ The `Variant` and `TypeInfo`, `NodeId`, `ExpandedNodeId`, `ExtensionObject`, `Lo 2. The default item can be created by assigning `default`, e.g. producing `NodeId.Null` for NodeId and `QualifiedName.Null` for QualifiedName. It is recommended to use the `Null` property on these types for readability and per your coding conventions. 3. Any API that mutated an instance of one of these built in types must be replaced with methods that return a new value of the type, e.g. `NodeId.WithNamespaceIndex(ushort)` as setters were removed. -### ByteString +## ByteString Previously the OPC UA built-in type *ByteString* was represented as `byte[]`. This caused ambiguities with regards to it and the byte *array* type. This has changed and `ByteString` is now a type in the Opc.Ua namespace. It is a wrapper around `ReadOnlyMemory` and while `Variant` handles both still interchangeably, the generated API now simplifies mixing of byte arrays and `ByteString` without confusion. @@ -29,7 +51,7 @@ To migrate, perform the following general replacements in your code: - If your code tried to set a byte in the ByteString, create a buffer `byte[]` and after changing convert to `ByteString` using `ByteString.From(buffer)` or `.ToByteString()` extension method - Perform changes only where you encounter build breaks. This should be enough to get into a working state. Later adjust the code as needed. -### ArrayOf and MatrixOf +## ArrayOf and MatrixOf Similar to `ByteString`, `ArrayOf` and `MatrixOf` are new type safe and sliceable generic value types representing non-scalar values. They are immutable meaning the values at an index inside them cannot be "set" unless they are converted to a `Span` (and then reconverted to a `ArrayOf`/`MatrixOf`). @@ -74,11 +96,11 @@ Note that equality operators and methods now compare the content of the Array an ArrayOf i = c.ConvertAll(v => (int)v); ``` -#### Configuration collection types removed +### Configuration collection types removed All `List`-based collection wrappers for configuration types have been removed and replaced with `ArrayOf`: `ServerSecurityPolicyCollection`, `TransportConfigurationCollection`, `SamplingRateGroupCollection`, `ReverseConnectClientCollection`, `ReverseConnectClientEndpointCollection`, `ServerRegistrationCollection`, `CertificateIdentifierCollection`, `CertificateGroupConfigurationCollection`, `OAuth2ServerSettingsCollection`, `OAuth2CredentialCollection`. -#### Generated data type fields with ValueRank=OneOrMoreDimensions +### Generated data type fields with ValueRank=OneOrMoreDimensions Previously, every structure field declared with `ValueRank="OneOrMoreDimensions"` in a model design was generated as `global::Opc.Ua.Variant`. The property is now typed as `global::Opc.Ua.MatrixOf` (mirroring the `ArrayOf` treatment already used for `ValueRank="Array"`). Encoding/decoding still flows through `Variant`, but the boxing/unboxing happens inside the encoder calls so consumers see the typed surface. @@ -115,7 +137,7 @@ The element type follows the field's `DataType`: - `IDecoder` gained a parameterless `ReadEncodeableMatrix(string? fieldName) where T : IEncodeable, new()` overload that mirrors the existing `ReadEncodeableArray(string? fieldName)` shape. Custom `IDecoder` implementations should add this overload alongside the existing encoding-id variant. -##### VariableType State classes, PropertyState instances, and service parameters +#### VariableType State classes, PropertyState instances, and service parameters The same `MatrixOf` opt-in now extends beyond structure data type fields to three sibling sites in the source generator: @@ -148,7 +170,7 @@ For *concrete* matrix variable types (today only `XYArrayItemType`) and matrix-r }.ToMatrixOf(2); ``` -### DateTimeUtc +## DateTimeUtc Previously the **DateTime** built in type was represented by the `System.DateTime` type. It is now represented by the `Opc.Ua.DateTimeUtc` type. This new type complies with the details of the spec without requiring external helper methods to be used. It's Value property returns the ticks, bounded by the information in Part 6 of the spec, and its time is always UTC. There are conversion operations to and from `DateTime`, but also `DateTimeOffset` and `long` and a minimal subset of `System.DateTime` API to allow for simpler porting. `DateTime` implicitly converts to `DateTimeUtc`, but not vice versa to force use of the new type. @@ -158,11 +180,11 @@ Previously the **DateTime** built in type was represented by the `System.DateTim - Replace `DateTime.UtcNow` with `DateTimeUtc.Now` for UTC time "right now". `DateTime.Now` or `DateTime.Today` can be cast or replaced with its Utc variant, which is likely intended anyway as all date/time values in OPC UA are UTC. - When assigning a `DateTime` value to a `DateTimeUtc` variable, add a cast, or use the corresponding `DateTimeUtc` constructor. -### QualifiedName and LocalizedText +## QualifiedName and LocalizedText There is no implicit conversion from `string` to `QualifiedName` or `LocalizedText` anymore. For one, it flags areas where null assignment is happening implicitly, and secondly, it makes the API more explicit. E.g. previously it was possible to assign a string to a browse name which landed the browse name accidentally in namespace 0 instead of the owning namespace. If you know what you are doing you can explicitly cast the string, but it is suggested to use the new static `From` API instead. -### StatusCode +## StatusCode `StatusCode` contains now not only a uint code, but also a symbol. Symbols are interned strings and using the `StatusCodes` constants therefore come with the symbol string. This removes the need to look up the symbolic id, however, when receiving a uint code it needs to be translated to a StatusCode constant to retain the Symbol. Older API has been obsoleted with proper instructions. Since types are immutable it is important to replace mutation calls with the proper replacement method and store the returned value. @@ -180,7 +202,7 @@ A matching `GetHashCode(StatusCodeComparison comparison)` overload is available When migrating code that compared `statusCode.Code` (the raw `uint`) against a `StatusCodes.XXX` constant, prefer the `==` operator (`statusCode == StatusCodes.XXX`) so the comparison ignores the non-code bits. Likewise replace comparisons against `statusCode.CodeBits` with the `==` operator. -### NodeId/ExpandedNodeId +## NodeId/ExpandedNodeId `NodeId`s with integer identifiers (the most common case) now do not box the integer identifier anymore into an object, making the entire NodeId heap allocation free (*). ExpandedNodeId with integer identifiers only contain an allocated namespace Uri, which is mostly a const (interned) string, reducing small allocations across both types. Because both types are now immutable, they must be mutated using the provided `With`. Access to the identifier in boxed form (object) is deprecated. Instead use the `TryGetValue(out uint/string/Guid/byte[])` API. If you need to get the identifier only to "stringify" it, use the `IdentifierAsText` property which avoids boxing integer identifiers. @@ -188,7 +210,7 @@ There is no implicit conversion from `uint`/`Guid`/`string`/`byte[]` to `NodeId` > (*) Note that NodeId leverages the new `uint` field to cache the HashCode of a "non-uint" "Identifier", which provides faster lookup using NodeId/ExpandedNodeId as key. -### Variant, DataValue and ExtensionObject +## Variant, DataValue and ExtensionObject Previously the `Variant` was a *mutable* struct containing a `TypeInfo` and `Value` property allowing setting the inner state and returning `object`. All value types thus were implicitly boxed to object and landing on the heap. The new `Variant` only boxes value types > 8 bytes in size (*), and stores the rest in a union. `TypeInfo`, previously a class, also now is stored as a 4 byte type (with padding). @@ -209,7 +231,7 @@ var output = session.Call(objectId, methodId, `null` arguments must be passed as `Variant.Null` (a literal `null` will not bind to the `params Variant[]` overload). -#### Deprecated boxing behavior +### Deprecated boxing behavior Access to the `Value` property of `Variant` is marked as [Obsolete] to discourage use in favor of casting to `` or `Get()` (both throw) or preferably `bool TryGetValue(out value)` calls. The same applies to the `Value` property of `DataValue`. The APIs perform any required conversion between `BuiltInType.Int32` and `BuiltInType.Enumeration` as well as arrays of `BuiltInType.Byte` and `BuiltInType.ByteString`. This also applies to the `Body` property of `ExtensionObject`. Here prefer the use of `TryGetValue` and `TryGetBinary, TryGetJson, TryGetXml`. @@ -223,7 +245,7 @@ To perform conversion from `` to a Variant, helper methods are available in ` > All other built in value types (`ExtensionObject`, `NodeId`, `QualifiedName`, `LocalizedText`, `Uuid`, etc.) are > 8 bytes in size and are therefore boxed when stored inside a Variant. > Future improvements will make certain types like `ArrayOf` be stored *spliced* inside the Variant (where the array pointer is stored in the object, and length/offset inside the union). -#### Replacement of all use of System.Object in generated code and API +### Replacement of all use of System.Object in generated code and API `Variant` is now the type reflecting the OPC UA Variant type in all API. That means all generated API now uses Variant instead of `System.Object` and all `Value` Properties are `Variant` too. This provides type safety and removes the need for Reflection via `GetType()` when the underlying type already is `Variant`. @@ -252,7 +274,7 @@ To migrate, perform the following general replacements in your code: - For Variable and VariableType node state classes that provide a narrowed "Value" via generic `` any access to `T Value` incurs a heavy type check. It is recommended to use `WrappedValue` instead when possible for assignment and access. - While most assignments work implicitly, use `TypeInfo.GetDefaultVariantValue` instead of `TypeInfo.GetDefaultValue` to initialize a variant value to a default that is `!= Variant.Null`. -### DataValue +## DataValue `DataValue` has been converted from a reference type (class) to a `readonly struct` to relieve GC pressure on hot subscription/encoder paths. The semantics are aligned with the other immutable built-in types (`NodeId`, `ExtensionObject`, etc.). @@ -310,13 +332,13 @@ async Task EnqueueAsync(DataValue dv) } ``` -### XmlElement +## XmlElement Previously the `XmlElement` built in type was represented by the `System.Xml.XmlElement` system type. While officially a deprecated, there is now a value type `XmlElement` that merely wraps a string but provides conversion operations to `System.Xml.XmlElement` and `System.Linq.Xml.XNode` as well as validation and equality/hashing operations. Normally you just need to remove `using System.Xml` and code continues working as is. If you need to have access to the `System.Xml.XmlElement` cast or use the `ToXmlElement` method. > `XmlElement` types are compared via a normalized version of the XML `string` contained, which removes all whitespace before comparing. This can result in some ambiguity, but operates well enough for test operations. For complete equality, cast to XNode and use `DeepEquals`. -### EnumValue to represent the enumeration built in type +## EnumValue to represent the enumeration built in type `EnumValue` bundles a symbol with a integer value (same as `StatusCode`). While most API works with standard .net `enum` types, these do not work in scenarios where the enum value is the result of a `EnumDefinition`. For these cases the `EnumValue` overloads provide a similar experience to using `enum`. In addition, the `EnumValue` type @@ -333,11 +355,11 @@ Variant v = new Variant(EnumValue.From(MyEnum.Value)); // or Variant v = Variant.From(MyEnum.Value); ``` -### ExtensionObject array helpers changed +## ExtensionObject array helpers changed `ExtensionObject.ToArray(object, Type)` and `ToList(object)` removed. Use `extensionObjects.GetStructuresOf()` or `ExtensionObject.ToArray(ArrayOf)`. -### Other Data Types +## Other Data Types All generated data types implementing `IEncodeable` are now equality comparable using `==` and `!=` and implement `IEquatable`. Equality defaults to the `IsEqual` implementation of the `IEncodeable` interface. In addition `ToString()` and `GetHashCode()` are implemented making all generated data types effectively equivalent to `record` classes with the exception of supporting `with` expressions. @@ -348,7 +370,7 @@ No changes are required, however there can be subtle bugs exposed, e.g.: - When comparing data type instances for reference equality, use `ReferenceEquals`, instead of `==` or `!=` operators. You can use the `RefEqualityComparer` helper when creating Dictionaries that use the type as key and require reference equality semantics for it. - When testing for `null`, use `is null` for more performant code. -### Obsoleted APIs and replacements +## Obsoleted APIs and replacements - `NodeId(string text)` -> `NodeId.Parse(string)` - `NodeId(object identifier, ushort namespaceIndex)` -> typed constructors: `new NodeId(uint, ushort)`, `new NodeId(Guid, ushort)`, `new NodeId(string, ushort)`, `new NodeId(ByteString, ushort)` @@ -376,7 +398,7 @@ No changes are required, however there can be subtle bugs exposed, e.g.: - `new DataValue(StatusCode)` and `new DataValue(StatusCode, DateTimeUtc)` -> use `DataValue.FromStatusCode(StatusCode)` and `DataValue.FromStatusCode(StatusCode, DateTimeUtc)`. The constructors suffered from a C# overload resolution bug where `new DataValue(42)` silently resolved to `DataValue(StatusCode)` instead of `DataValue(Variant)`, losing the value. - `SessionManager.ImpersonateUser` -> register `IUserTokenAuthenticator` instances via `services.AddIdentityAuthenticator()` or `server.CurrentInstance.IdentityRegistry.Register(...)`. The event remains functional as a fallback, but is now `[Obsolete]`; the in-box ReferenceServer, GlobalDiscoverySampleServer, and ConsoleReferenceClient samples use the provider model. -### APIs permanently removed +## APIs permanently removed - All `Collection` classes, e.g. Int32Collection or ArgumentCollection -> use `List` or `ArrayOf` - `ICloneable`/`Clone()`/`MemberwiseClone()` on the immutable built-in types -> use assignment for copies diff --git a/samples/MinimalApi/MinimalClient/Program.cs b/samples/MinimalApi/MinimalClient/Program.cs index 34aceca11d..50082bfbce 100644 --- a/samples/MinimalApi/MinimalClient/Program.cs +++ b/samples/MinimalApi/MinimalClient/Program.cs @@ -254,6 +254,19 @@ static async Task RunClientAsync(IServiceProvider services) } } + Console.WriteLine(); + Console.WriteLine("Waiting for a data change notification..."); + try + { + await handler.FirstDataChange + .WaitAsync(TimeSpan.FromSeconds(10), cancellationToken) + .ConfigureAwait(false); + } + catch (TimeoutException) + { + Console.WriteLine("No data change notification arrived within 10 seconds."); + } + Console.WriteLine(); Console.WriteLine("Disconnecting..."); } @@ -262,8 +275,17 @@ static async Task RunClientAsync(IServiceProvider services) Console.WriteLine("Done"); } +/// +/// Prints subscription notifications to the console and signals when the first +/// data change notification arrives. +/// internal sealed class ConsoleSubscriptionHandler : ISubscriptionNotificationHandler { + /// + /// Completes when the first data change notification has been received. + /// + public Task FirstDataChange => m_firstDataChange.Task; + public ValueTask OnDataChangeNotificationAsync( ISubscription subscription, uint sequenceNumber, @@ -276,6 +298,10 @@ internal sealed class ConsoleSubscriptionHandler : ISubscriptionNotificationHand { Console.WriteLine($"Subscription value: {change.Value.WrappedValue}"); } + if (!notification.IsEmpty) + { + m_firstDataChange.TrySetResult(); + } return default; } @@ -309,4 +335,7 @@ internal sealed class ConsoleSubscriptionHandler : ISubscriptionNotificationHand Console.WriteLine($"Subscription state: {state}"); return default; } + + private readonly TaskCompletionSource m_firstDataChange = + new(TaskCreationOptions.RunContinuationsAsynchronously); } diff --git a/samples/MinimalApi/MinimalClient/README.md b/samples/MinimalApi/MinimalClient/README.md index 1a1cb39bab..3ec79a75c8 100644 --- a/samples/MinimalApi/MinimalClient/README.md +++ b/samples/MinimalApi/MinimalClient/README.md @@ -61,7 +61,8 @@ The minimal client demonstrates the following operations: 6. **Alarms & Conditions**: Resolves `AlarmClientFactory` and creates an A&C client for the session 7. **Browsing**: Browses the server's address space (`ObjectsFolder`) 8. **Reading**: Reads the server's current time from the StandardServer -9. **Clean Shutdown**: Properly closes the subscription, session, and host +9. **Notifications**: Waits up to 10 seconds for the subscription's first data change notification +10. **Clean Shutdown**: Properly closes the subscription, session, and host ## Architecture diff --git a/samples/PubSub/ConsoleReferencePubSubClient/README.md b/samples/PubSub/ConsoleReferencePubSubClient/README.md index ba5734f9cd..e03985a135 100644 --- a/samples/PubSub/ConsoleReferencePubSubClient/README.md +++ b/samples/PubSub/ConsoleReferencePubSubClient/README.md @@ -36,6 +36,21 @@ ConsoleReferencePubSubClient subscriber --profile udp-uadp Options: `--profile`, `--config-file `, `--publisher-id-filter`, `--writer-group-id-filter`, `--data-set-writer-id-filter`, `--endpoint`. +### Publisher and subscriber on one computer + +The default UDP endpoint, `opc.udp://239.0.0.1:4840`, is a multicast address. +Multicast loopback is disabled by default, so a subscriber on the publisher's +computer can miss the messages. To run both on one computer, give both the +unicast loopback endpoint: + +```bash +ConsoleReferencePubSubClient subscriber --profile udp-uadp --endpoint opc.udp://127.0.0.1:4840 +ConsoleReferencePubSubClient publisher --profile udp-uadp --endpoint opc.udp://127.0.0.1:4840 +``` + +See [Run a first publisher and subscriber](../../../docs/PubSub.md#run-a-first-publisher-and-subscriber) +for the expected output and the settings that must match. + ### `external` Bridges an external OPC UA server (defaults to the repository's ConsoleReferenceServer diff --git a/src/Opc.Ua.Client/Session/Subscription/DefaultSubscriptionEngine.cs b/src/Opc.Ua.Client/Session/Subscription/DefaultSubscriptionEngine.cs index d89f19fbb8..af5b764a68 100644 --- a/src/Opc.Ua.Client/Session/Subscription/DefaultSubscriptionEngine.cs +++ b/src/Opc.Ua.Client/Session/Subscription/DefaultSubscriptionEngine.cs @@ -307,7 +307,7 @@ namespace Opc.Ua.Client /// needed by the modern /// base class. /// - private sealed class SubscriptionContextAdapter : ISubscriptionContext + internal sealed class SubscriptionContextAdapter : ISubscriptionContext { public SubscriptionContextAdapter(ISubscriptionEngineContext context) { @@ -316,8 +316,22 @@ namespace Opc.Ua.Client } /// + /// + /// The revised session timeout comes from the server. A value that + /// is not a positive number of at most + /// milliseconds is reported as , so a + /// malformed server value cannot fail subscription creation. + /// public TimeSpan SessionTimeout - => TimeSpan.FromMilliseconds(m_context.OperationTimeout); + { + get + { + double sessionTimeout = m_context.SessionTimeout; + return sessionTimeout is > 0 and <= int.MaxValue + ? TimeSpan.FromMilliseconds(sessionTimeout) + : TimeSpan.Zero; + } + } /// public ISubscriptionServiceSetClientMethods SubscriptionServiceSet => m_services; diff --git a/src/Opc.Ua.Client/Session/Subscription/ISubscriptionEngineContext.cs b/src/Opc.Ua.Client/Session/Subscription/ISubscriptionEngineContext.cs index c6043bfe3c..52ac22c426 100644 --- a/src/Opc.Ua.Client/Session/Subscription/ISubscriptionEngineContext.cs +++ b/src/Opc.Ua.Client/Session/Subscription/ISubscriptionEngineContext.cs @@ -98,6 +98,13 @@ namespace Opc.Ua.Client /// int OperationTimeout { get; } + /// + /// The session timeout revised by the server in milliseconds, or 0 + /// before the session has been created. Subscriptions compare their + /// lifetime with this value. + /// + double SessionTimeout { get; } + /// /// The current server state as known by the session. /// diff --git a/src/Opc.Ua.Client/Session/Subscription/SessionEngineContext.cs b/src/Opc.Ua.Client/Session/Subscription/SessionEngineContext.cs index f73127bbd2..3a8a98c29f 100644 --- a/src/Opc.Ua.Client/Session/Subscription/SessionEngineContext.cs +++ b/src/Opc.Ua.Client/Session/Subscription/SessionEngineContext.cs @@ -80,6 +80,10 @@ namespace Opc.Ua.Client public int OperationTimeout => m_session.OperationTimeout; + /// + public double SessionTimeout + => m_session.SessionTimeout; + /// public ServerState ServerState => m_session.m_serverState; diff --git a/src/Opc.Ua.PubSub/Application/PubSubApplication.cs b/src/Opc.Ua.PubSub/Application/PubSubApplication.cs index 095a81560d..1b8a3f3ee7 100644 --- a/src/Opc.Ua.PubSub/Application/PubSubApplication.cs +++ b/src/Opc.Ua.PubSub/Application/PubSubApplication.cs @@ -1833,6 +1833,70 @@ namespace Opc.Ua.PubSub.Application } } } + + RegisterDataSetReaderMetaData(); + } + + /// + /// Registers the DataSetMetaData configured on each DataSetReader, which + /// provides the information needed to decode its DataSetMessages + /// (Part 14 §6.2.9.4). A Subscriber can therefore decode RawData + /// messages without first receiving a metadata announcement, for example + /// when announcements are not sent or are dropped by a secured reader. + /// + /// + /// The registry is keyed by the exact DataSetWriter identity, so readers + /// whose PublisherId, WriterGroupId, or DataSetWriterId filter is a + /// wildcard are skipped. An existing entry, such as the metadata of a + /// PublishedDataSet in the same application, is kept; a later metadata + /// announcement replaces the registered entry. + /// + private void RegisterDataSetReaderMetaData() + { + foreach (PubSubConnection connection in m_connections) + { + for (int readerGroupIndex = 0; + readerGroupIndex < connection.ReaderGroups.Count; + readerGroupIndex++) + { + if (connection.ReaderGroups[readerGroupIndex] is not ReaderGroup readerGroup) + { + continue; + } + for (int readerIndex = 0; + readerIndex < readerGroup.DataSetReaders.Count; + readerIndex++) + { + if (readerGroup.DataSetReaders[readerIndex] is not DataSetReader reader) + { + continue; + } + + DataSetMetaDataType? metaData = reader.Configuration.DataSetMetaData; + if (metaData is null || + reader.ExpectedPublisherId.IsNull || + reader.WriterGroupId == 0 || + reader.DataSetWriterId == 0) + { + continue; + } + + ConfigurationVersionDataType version = + metaData.ConfigurationVersion + ?? new ConfigurationVersionDataType(); + var key = new DataSetMetaDataKey( + reader.ExpectedPublisherId, + reader.WriterGroupId, + reader.DataSetWriterId, + metaData.DataSetClassId, + version.MajorVersion); + if (MetaDataRegistry.TryGet(key, out _) == MetaDataMatchResult.NotFound) + { + MetaDataRegistry.Register(key, metaData); + } + } + } + } } private async ValueTask ApplyMutationAsync( diff --git a/src/Opc.Ua.PubSub/Encoding/Uadp/UadpDecoder.cs b/src/Opc.Ua.PubSub/Encoding/Uadp/UadpDecoder.cs index 4df633eb77..3a486885d6 100644 --- a/src/Opc.Ua.PubSub/Encoding/Uadp/UadpDecoder.cs +++ b/src/Opc.Ua.PubSub/Encoding/Uadp/UadpDecoder.cs @@ -845,7 +845,9 @@ out PubSubDataSetMessageType messageType)) DataSetMetaDataType? metaData = ResolveMetaData( publisherId, writerGroupId, writerId, dataSetClassId, - majorVersion, context); + majorVersion, + (contentMask & UadpDataSetMessageContentMask.MajorVersion) != 0, + context); ArrayOf? fields = UadpFieldDecoder.DecodeFields( ref reader, encoding, messageType, metaData, context.MessageContext); @@ -880,6 +882,7 @@ out PubSubDataSetMessageType messageType)) ushort writerId, Uuid dataSetClassId, uint majorVersion, + bool hasMajorVersion, PubSubNetworkMessageContext context) { var key = new DataSetMetaDataKey( @@ -889,6 +892,13 @@ out PubSubDataSetMessageType messageType)) key, out DataSetMetaDataType? metaData); if (result == MetaDataMatchResult.MajorVersionMismatch) { + // A DataSetMessage without a ConfigurationVersion cannot be + // compared with the metadata, so the registered metadata of + // the DataSetWriter applies (Part 14 §6.2.9.4). + if (!hasMajorVersion) + { + return metaData; + } context.Diagnostics.Increment( PubSubDiagnosticsCounterKind.ResolverErrors); return null; diff --git a/src/Opc.Ua.PubSub/MetaData/IDataSetMetaDataRegistry.cs b/src/Opc.Ua.PubSub/MetaData/IDataSetMetaDataRegistry.cs index 5383918110..8dd38f4c1e 100644 --- a/src/Opc.Ua.PubSub/MetaData/IDataSetMetaDataRegistry.cs +++ b/src/Opc.Ua.PubSub/MetaData/IDataSetMetaDataRegistry.cs @@ -63,9 +63,10 @@ namespace Opc.Ua.PubSub.MetaData /// resolved metadata description. On /// the /// currently registered description for the same - /// PublisherId/WriterGroupId/DataSetWriterId is returned for - /// diagnostics; the caller must still reject the payload. On - /// , + /// PublisherId/WriterGroupId/DataSetWriterId. A caller must reject + /// a payload that carries a different MajorVersion; a payload without + /// a ConfigurationVersion can only be decoded with this description. + /// On , /// . /// /// The match classification. diff --git a/tests/Opc.Ua.Client.Tests/Session/DefaultSubscriptionEngineTests.cs b/tests/Opc.Ua.Client.Tests/Session/DefaultSubscriptionEngineTests.cs index 9f54785c00..5d6add9db2 100644 --- a/tests/Opc.Ua.Client.Tests/Session/DefaultSubscriptionEngineTests.cs +++ b/tests/Opc.Ua.Client.Tests/Session/DefaultSubscriptionEngineTests.cs @@ -130,6 +130,44 @@ namespace Opc.Ua.Client.Tests Throws.TypeOf()); } + [Test] + public void SubscriptionContextReportsRevisedSessionTimeout() + { + m_mockContext + .Setup(c => c.OperationTimeout) + .Returns(120000); + m_mockContext + .Setup(c => c.SessionTimeout) + .Returns(60000); + + var context = + new DefaultSubscriptionEngine.SubscriptionContextAdapter( + m_mockContext.Object); + + Assert.That( + context.SessionTimeout, + Is.EqualTo(TimeSpan.FromSeconds(60))); + } + + [TestCase(0.0)] + [TestCase(-1.0)] + [TestCase(double.NaN)] + [TestCase(double.PositiveInfinity)] + [TestCase(1e300)] + public void SubscriptionContextReportsZeroForUnusableSessionTimeout( + double sessionTimeout) + { + m_mockContext + .Setup(c => c.SessionTimeout) + .Returns(sessionTimeout); + + var context = + new DefaultSubscriptionEngine.SubscriptionContextAdapter( + m_mockContext.Object); + + Assert.That(context.SessionTimeout, Is.EqualTo(TimeSpan.Zero)); + } + [Test] public void StartPublishingDoesNotThrow() { diff --git a/tests/Opc.Ua.Client.Tests/Session/SessionEngineContextTests.cs b/tests/Opc.Ua.Client.Tests/Session/SessionEngineContextTests.cs index b263e4b28e..1dc0f7bdd2 100644 --- a/tests/Opc.Ua.Client.Tests/Session/SessionEngineContextTests.cs +++ b/tests/Opc.Ua.Client.Tests/Session/SessionEngineContextTests.cs @@ -63,6 +63,7 @@ namespace Opc.Ua.Client.Tests Assert.That(context.Disposed, Is.False); Assert.That(context.DeleteSubscriptionsOnClose, Is.True); Assert.That(context.OperationTimeout, Is.Zero); + Assert.That(context.SessionTimeout, Is.EqualTo(scope.Session.SessionTimeout)); Assert.That(context.ServerState, Is.EqualTo(ServerState.Running)); Assert.That(context.ReturnDiagnostics, Is.EqualTo(DiagnosticsMasks.None)); Assert.That(context.Telemetry, Is.SameAs(scope.Telemetry)); diff --git a/tests/Opc.Ua.Client.Tests/Stack/Client/Fakes/ObservingSubscriptionEngineFactory.cs b/tests/Opc.Ua.Client.Tests/Stack/Client/Fakes/ObservingSubscriptionEngineFactory.cs index 154db73c50..6f3b873959 100644 --- a/tests/Opc.Ua.Client.Tests/Stack/Client/Fakes/ObservingSubscriptionEngineFactory.cs +++ b/tests/Opc.Ua.Client.Tests/Stack/Client/Fakes/ObservingSubscriptionEngineFactory.cs @@ -129,6 +129,9 @@ namespace Opc.Ua.Client.Tests.Stack.Client.Fakes /// public int OperationTimeout => context.OperationTimeout; + /// + public double SessionTimeout => context.SessionTimeout; + /// public ServerState ServerState => context.ServerState; diff --git a/tests/Opc.Ua.PubSub.Tests/Application/SubscriberDataSetMetaDataTests.cs b/tests/Opc.Ua.PubSub.Tests/Application/SubscriberDataSetMetaDataTests.cs new file mode 100644 index 0000000000..c2baf44fcb --- /dev/null +++ b/tests/Opc.Ua.PubSub.Tests/Application/SubscriberDataSetMetaDataTests.cs @@ -0,0 +1,512 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using NUnit.Framework; +using Opc.Ua.PubSub.Application; +using Opc.Ua.PubSub.Configuration; +using Opc.Ua.PubSub.DataSets; +using Opc.Ua.PubSub.Encoding; +using Opc.Ua.PubSub.MetaData; +using Opc.Ua.PubSub.Security; +using Opc.Ua.PubSub.Security.Policies; +using Opc.Ua.PubSub.Transports; +using Opc.Ua.Tests; + +namespace Opc.Ua.PubSub.Tests.Application +{ + /// + /// Verifies that a Subscriber uses the DataSetMetaData configured on its + /// DataSetReader to decode RawData DataSetMessages when it runs separately + /// from the Publisher and shares no metadata registry with it, per + /// + /// Part 14 §6.2.9.4. + /// + [TestFixture] + [TestSpec("6.2.9.4")] + [CancelAfter(30000)] + public sealed class SubscriberDataSetMetaDataTests + { + private const string Address = "opc.udp://239.0.0.1:4840"; + private const string SecurityGroupId = "SubscriberMetaDataGroup"; + private const string DataSetName = "Simple"; + private const string ReaderName = "Reader 1"; + private const ushort PublisherIdValue = 1; + private const ushort WriterGroupId = 100; + private const ushort DataSetWriterId = 1; + + [TestCase(MessageSecurityMode.SignAndEncrypt)] + [TestCase(MessageSecurityMode.None)] + public async Task SeparateSubscriberDecodesRawDataWithConfiguredMetaDataAsync( + MessageSecurityMode securityMode) + { + var bus = new InMemoryBus(); + var sink = new FirstDataSetSink(); + + await using IPubSubApplication subscriber = CreateBuilder(bus, securityMode) + .WithApplicationId("urn:test:subscriber") + .UseConfiguration(CreateSubscriberConfiguration( + securityMode, PublisherIdValue, WriterGroupId)) + .AddSubscribedDataSetSink(ReaderName, sink) + .Build(); + await using IPubSubApplication publisher = CreateBuilder(bus, securityMode) + .WithApplicationId("urn:test:publisher") + .UseConfiguration(CreatePublisherConfiguration(securityMode)) + .AddDataSetSource(DataSetName, new CounterSource()) + .Build(); + + await subscriber.StartAsync().ConfigureAwait(false); + await publisher.StartAsync().ConfigureAwait(false); + + IReadOnlyList fields = await sink.FirstDataSet + .WaitAsync(TimeSpan.FromSeconds(10)) + .ConfigureAwait(false); + + Assert.That(fields, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(fields[0].Name, Is.EqualTo("Counter")); + Assert.That(fields[0].Value.TryGetValue(out int counter), Is.True); + Assert.That(counter, Is.GreaterThan(0)); + Assert.That(fields[1].Name, Is.EqualTo("Toggle")); + }); + } + + [Test] + public async Task BuildRegistersMetaDataOfReaderWithExactFilterAsync() + { + await using IPubSubApplication subscriber = CreateBuilder( + new InMemoryBus(), MessageSecurityMode.None) + .WithApplicationId("urn:test:subscriber") + .UseConfiguration(CreateSubscriberConfiguration( + MessageSecurityMode.None, PublisherIdValue, WriterGroupId)) + .Build(); + + MetaDataMatchResult result = subscriber.MetaDataRegistry.TryGet( + ReaderKey(WriterGroupId), + out DataSetMetaDataType? metaData); + + Assert.That(result, Is.EqualTo(MetaDataMatchResult.Match)); + Assert.That(metaData!.Fields.Count, Is.EqualTo(2)); + } + + [Test] + public async Task BuildDoesNotRegisterMetaDataOfReaderWithWildcardFilterAsync() + { + await using IPubSubApplication subscriber = CreateBuilder( + new InMemoryBus(), MessageSecurityMode.None) + .WithApplicationId("urn:test:subscriber") + .UseConfiguration(CreateSubscriberConfiguration( + MessageSecurityMode.None, PublisherIdValue, writerGroupId: 0)) + .Build(); + + Assert.That(subscriber.MetaDataRegistry.Keys.Count, Is.Zero); + } + + [Test] + public async Task ReaderMetaDataDoesNotReplacePublishedDataSetMetaDataAsync() + { + PubSubConfigurationDataType configuration = + CreatePublisherConfiguration(MessageSecurityMode.None); + PubSubConfigurationDataType readers = CreateSubscriberConfiguration( + MessageSecurityMode.None, PublisherIdValue, WriterGroupId); + configuration.Connections[0].ReaderGroups = readers.Connections[0].ReaderGroups; + configuration.Connections[0].ReaderGroups[0].DataSetReaders[0] + .DataSetMetaData.Name = "ReaderView"; + + await using IPubSubApplication application = CreateBuilder( + new InMemoryBus(), MessageSecurityMode.None) + .WithApplicationId("urn:test:loopback") + .UseConfiguration(configuration) + .AddDataSetSource(DataSetName, new CounterSource()) + .Build(); + + application.MetaDataRegistry.TryGet( + ReaderKey(WriterGroupId), + out DataSetMetaDataType? metaData); + + Assert.That(metaData!.Name, Is.EqualTo(DataSetName)); + } + + private static DataSetMetaDataKey ReaderKey(ushort writerGroupId) + { + return new DataSetMetaDataKey( + PublisherId.FromUInt16(PublisherIdValue), + writerGroupId, + DataSetWriterId, + Uuid.Empty, + 1); + } + + private static PubSubApplicationBuilder CreateBuilder( + InMemoryBus bus, + MessageSecurityMode securityMode) + { + PubSubApplicationBuilder builder = new PubSubApplicationBuilder( + NUnitTelemetryContext.Create()) + .UseAllStandardEncoders() + .AddTransportFactory(new InMemoryBusTransportFactory(bus)); + if (securityMode != MessageSecurityMode.None) + { + builder.AddSecurityKeyProvider(CreateKeyProvider()); + } + return builder; + } + + private static PubSubConfigurationDataType CreatePublisherConfiguration( + MessageSecurityMode securityMode) + { + return PubSubConfigurationBuilder.Create() + .AddPublishedDataSet(DataSetName, dataSet => dataSet + .AddField("Counter", (byte)BuiltInType.Int32, DataTypeIds.Int32) + .AddField("Toggle", (byte)BuiltInType.Boolean, DataTypeIds.Boolean)) + .AddConnection("Publisher Connection", connection => connection + .WithPublisherId(new Variant(PublisherIdValue)) + .WithTransportProfile(Profiles.PubSubUdpUadpTransport) + .WithAddress(Address) + .AddWriterGroup("WriterGroup 1", group => + { + group + .WithWriterGroupId(WriterGroupId) + .WithPublishingInterval(50) + .WithMessageSettings(new UadpWriterGroupMessageDataType + { + DataSetOrdering = DataSetOrderingType.AscendingWriterId, + NetworkMessageContentMask = (uint)NetworkMessageContentMask + }) + .WithTransportSettings(new DatagramWriterGroupTransportDataType()); + if (securityMode != MessageSecurityMode.None) + { + group.WithSecurity( + securityMode, + SecurityGroupId, + "opc.tcp://localhost:4840/SecurityKeyService"); + } + group.AddDataSetWriter("Writer 1", writer => writer + .WithDataSetWriterId(DataSetWriterId) + .WithDataSetName(DataSetName) + .WithKeyFrameCount(1) + .WithFieldContentMask(DataSetFieldContentMask.RawData) + .WithMessageSettings(new UadpDataSetWriterMessageDataType + { + DataSetMessageContentMask = (uint)DataSetMessageContentMask + })); + })) + .Build(); + } + + private static PubSubConfigurationDataType CreateSubscriberConfiguration( + MessageSecurityMode securityMode, + ushort publisherId, + ushort writerGroupId) + { + return PubSubConfigurationBuilder.Create() + .AddConnection("Subscriber Connection", connection => connection + .WithPublisherId(new Variant(publisherId)) + .WithTransportProfile(Profiles.PubSubUdpUadpTransport) + .WithAddress(Address) + .AddReaderGroup("ReaderGroup 1", group => + { + if (securityMode != MessageSecurityMode.None) + { + group.WithSecurity( + securityMode, + SecurityGroupId, + "opc.tcp://localhost:4840/SecurityKeyService"); + } + group.AddDataSetReader(ReaderName, reader => reader + .WithFilter(new Variant(publisherId), writerGroupId, DataSetWriterId) + .WithFieldContentMask(DataSetFieldContentMask.RawData) + .WithMessageReceiveTimeout(5000) + .WithMirrorSubscribedDataSet(ReaderName) + .WithMessageSettings(new UadpDataSetReaderMessageDataType + { + NetworkMessageContentMask = (uint)NetworkMessageContentMask, + DataSetMessageContentMask = (uint)DataSetMessageContentMask + }) + .WithDataSetMetaData(DataSetName, metaData => metaData + .WithoutFieldIds() + .AddField("Counter", (byte)BuiltInType.Int32, DataTypeIds.Int32) + .AddField("Toggle", (byte)BuiltInType.Boolean, DataTypeIds.Boolean))); + })) + .Build(); + } + + private static UadpNetworkMessageContentMask NetworkMessageContentMask => + UadpNetworkMessageContentMask.PublisherId | + UadpNetworkMessageContentMask.GroupHeader | + UadpNetworkMessageContentMask.WriterGroupId | + UadpNetworkMessageContentMask.PayloadHeader | + UadpNetworkMessageContentMask.NetworkMessageNumber | + UadpNetworkMessageContentMask.SequenceNumber; + + // No MajorVersion or MinorVersion: the Subscriber cannot compare versions. + private static UadpDataSetMessageContentMask DataSetMessageContentMask => + UadpDataSetMessageContentMask.Status | + UadpDataSetMessageContentMask.SequenceNumber; + + private static StaticSecurityKeyProvider CreateKeyProvider() + { + PubSubAes256CtrPolicy policy = PubSubAes256CtrPolicy.Instance; + byte[] signing = new byte[policy.SigningKeyLength]; + byte[] encrypting = new byte[policy.EncryptingKeyLength]; + byte[] nonce = new byte[policy.NonceLength]; + for (int i = 0; i < signing.Length; i++) + { + signing[i] = (byte)(i + 1); + } + for (int i = 0; i < encrypting.Length; i++) + { + encrypting[i] = (byte)(i + 100); + } + for (int i = 0; i < nonce.Length; i++) + { + nonce[i] = (byte)(i + 200); + } + + var key = new PubSubSecurityKey( + 1U, + ByteString.Create(signing), + ByteString.Create(encrypting), + ByteString.Create(nonce), + DateTimeUtc.From(DateTime.UtcNow), + TimeSpan.FromMinutes(60)); + var ring = new PubSubSecurityKeyRing(SecurityGroupId); + ring.SetCurrent(key); + return new StaticSecurityKeyProvider(SecurityGroupId, ring); + } + + /// + /// Produces an increasing counter and a toggle for each sample. + /// + private sealed class CounterSource : IPublishedDataSetSource + { + public DataSetMetaDataType BuildMetaData() + { + return new DataSetMetaDataType + { + Name = DataSetName, + DataSetClassId = Uuid.Empty, + Fields = + [ + new FieldMetaData + { + Name = "Counter", + BuiltInType = (byte)BuiltInType.Int32, + DataType = DataTypeIds.Int32, + ValueRank = ValueRanks.Scalar + }, + new FieldMetaData + { + Name = "Toggle", + BuiltInType = (byte)BuiltInType.Boolean, + DataType = DataTypeIds.Boolean, + ValueRank = ValueRanks.Scalar + } + ], + ConfigurationVersion = new ConfigurationVersionDataType + { + MajorVersion = 1, + MinorVersion = 0 + } + }; + } + + public ValueTask SampleAsync( + DataSetMetaDataType metaData, + CancellationToken cancellationToken = default) + { + int counter = Interlocked.Increment(ref m_counter); + ArrayOf fields = + [ + new DataSetField { Name = "Counter", Value = new Variant(counter) }, + new DataSetField { Name = "Toggle", Value = new Variant((counter & 1) == 0) } + ]; + return new ValueTask( + new PublishedDataSetSnapshot( + metaData.ConfigurationVersion ?? new ConfigurationVersionDataType(), + fields, + DateTimeUtc.From(DateTime.UtcNow))); + } + + private int m_counter; + } + + /// + /// Completes with the fields of the first received DataSet. + /// + private sealed class FirstDataSetSink : ISubscribedDataSetSink + { + public Task> FirstDataSet => m_first.Task; + + public ValueTask WriteAsync( + IReadOnlyList fields, + CancellationToken cancellationToken = default) + { + m_first.TrySetResult(fields); + return default; + } + + private readonly TaskCompletionSource> m_first = + new(TaskCreationOptions.RunContinuationsAsynchronously); + } + + /// + /// Delivers every frame sent by any transport of the bus to every + /// transport of the bus, like a datagram group on one host. + /// + private sealed class InMemoryBus + { + public void Attach(Channel receiver) + { + lock (m_lock) + { + m_receivers.Add(receiver); + } + } + + public void Detach(Channel receiver) + { + lock (m_lock) + { + m_receivers.Remove(receiver); + } + } + + public void Send(ReadOnlyMemory payload) + { + Channel[] receivers; + lock (m_lock) + { + receivers = [.. m_receivers]; + } + foreach (Channel receiver in receivers) + { + receiver.Writer.TryWrite(payload.ToArray()); + } + } + + private readonly Lock m_lock = new(); + private readonly List> m_receivers = []; + } + + private sealed class InMemoryBusTransportFactory : IPubSubTransportFactory + { + public InMemoryBusTransportFactory(InMemoryBus bus) + { + m_bus = bus; + } + + public string TransportProfileUri => Profiles.PubSubUdpUadpTransport; + + public IPubSubTransport Create( + PubSubConnectionDataType connection, + ITelemetryContext telemetry, + TimeProvider timeProvider) + { + return new InMemoryBusTransport(m_bus); + } + + private readonly InMemoryBus m_bus; + } + + private sealed class InMemoryBusTransport : IPubSubTransport + { + public InMemoryBusTransport(InMemoryBus bus) + { + m_bus = bus; + } + + public string TransportProfileUri => Profiles.PubSubUdpUadpTransport; + + public PubSubTransportDirection Direction => PubSubTransportDirection.SendReceive; + + public bool IsConnected { get; private set; } + + public event EventHandler? StateChanged + { + add { } + remove { } + } + + public ValueTask OpenAsync(CancellationToken cancellationToken = default) + { + m_bus.Attach(m_received); + IsConnected = true; + return default; + } + + public ValueTask CloseAsync(CancellationToken cancellationToken = default) + { + m_bus.Detach(m_received); + IsConnected = false; + return default; + } + + public ValueTask SendAsync( + ReadOnlyMemory payload, + string? topic = null, + CancellationToken cancellationToken = default) + { + m_bus.Send(payload); + return default; + } + + public async IAsyncEnumerable ReceiveAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + while (await m_received.Reader.WaitToReadAsync(cancellationToken) + .ConfigureAwait(false)) + { + while (m_received.Reader.TryRead(out byte[]? frame)) + { + yield return new PubSubTransportFrame( + frame, null, DateTimeUtc.From(DateTime.UtcNow)); + } + } + } + + public ValueTask DisposeAsync() + { + m_bus.Detach(m_received); + m_received.Writer.TryComplete(); + IsConnected = false; + return default; + } + + private readonly InMemoryBus m_bus; + private readonly Channel m_received = Channel.CreateUnbounded(); + } + } +} diff --git a/tests/Opc.Ua.PubSub.Tests/Encoding/Uadp/UadpEdgeCasesTests.cs b/tests/Opc.Ua.PubSub.Tests/Encoding/Uadp/UadpEdgeCasesTests.cs index f09b3b5c97..fe8c000a27 100644 --- a/tests/Opc.Ua.PubSub.Tests/Encoding/Uadp/UadpEdgeCasesTests.cs +++ b/tests/Opc.Ua.PubSub.Tests/Encoding/Uadp/UadpEdgeCasesTests.cs @@ -260,6 +260,81 @@ namespace Opc.Ua.PubSub.Tests.Encoding.Uadp "match the registered metadata."); } + [Test] + public async Task RawDataWithoutWireMajorVersionDecodesWithRegisteredMetaDataAsync() + { + // The Publisher encodes RawData with its versioned metadata but + // omits the ConfigurationVersion from the DataSetMessage header. + // Without a version to compare, the Subscriber decodes with the + // registered DataSetMetaData (Part 14 §6.2.9.4). + var registry = new DataSetMetaDataRegistry(); + var diag = new PubSubDiagnostics(PubSubDiagnosticsLevel.Low); + PubSubNetworkMessageContext context = + UadpTestUtilities.NewContext(registry, diag); + registry.Register( + new DataSetMetaDataKey( + PublisherId.FromByte(1), 7, 100, + (Uuid)Guid.Empty, 1), + new DataSetMetaDataType + { + Name = "Versioned", + ConfigurationVersion = new ConfigurationVersionDataType + { + MajorVersion = 1, + MinorVersion = 0 + }, + Fields = + [ + new FieldMetaData + { + Name = "f0", + BuiltInType = (byte)BuiltInType.UInt32, + ValueRank = ValueRanks.Scalar + } + ] + }); + var message = new UadpNetworkMessage + { + ContentMask = UadpNetworkMessageContentMask.PublisherId | + UadpNetworkMessageContentMask.GroupHeader | + UadpNetworkMessageContentMask.WriterGroupId | + UadpNetworkMessageContentMask.PayloadHeader, + PublisherId = PublisherId.FromByte(1), + WriterGroupId = 7, + DataSetMessages = + [ + new UadpDataSetMessage + { + DataSetWriterId = 100, + ContentMask = UadpDataSetMessageContentMask.SequenceNumber, + SequenceNumber = 1, + MetaDataVersion = new ConfigurationVersionDataType + { + MajorVersion = 1, + MinorVersion = 0 + }, + FieldEncoding = PubSubFieldEncoding.RawData, + Fields = [new DataSetField { Value = (Variant)123u }] + } + ] + }; + ReadOnlyMemory bytes = + await new UadpEncoder().EncodeAsync(message, context).ConfigureAwait(false); + + PubSubNetworkMessage? decoded = UadpDecoder.Decode(bytes, context); + + Assert.That(decoded, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That( + decoded!.DataSetMessages[0].Fields[0].Value, + Is.EqualTo((Variant)123u)); + Assert.That( + diag.Read(PubSubDiagnosticsCounterKind.ResolverErrors), + Is.Zero); + }); + } + [Test] public async Task ReceivedNetworkMessages_CounterIncrements() { diff --git a/tests/Opc.Ua.Server.Tests/Fluent/Assets/InlineMethod.xml b/tests/Opc.Ua.Server.Tests/Fluent/Assets/InlineMethod.xml new file mode 100644 index 0000000000..a8af4ded9c --- /dev/null +++ b/tests/Opc.Ua.Server.Tests/Fluent/Assets/InlineMethod.xml @@ -0,0 +1,50 @@ + + + + + + + urn:opcfoundation.org:2026-09:InlineMethod + http://opcfoundation.org/UA/ + + + + + + + + + + + + + + + + + + + ua:Organizes + ua:ObjectsFolder + + + + diff --git a/tests/Opc.Ua.Server.Tests/Fluent/InlineMethodIntegrationTests.cs b/tests/Opc.Ua.Server.Tests/Fluent/InlineMethodIntegrationTests.cs new file mode 100644 index 0000000000..3d8b504464 --- /dev/null +++ b/tests/Opc.Ua.Server.Tests/Fluent/InlineMethodIntegrationTests.cs @@ -0,0 +1,195 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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.IO; +using System.Threading.Tasks; +using InlineMethodModel; +using NUnit.Framework; +using Opc.Ua.Server.TestFramework; + +namespace Opc.Ua.Server.Tests.Fluent +{ + /// + /// End-to-end tests for a source-generated node manager whose model + /// method declares its arguments inline, without a method type. + /// + [TestFixture] + [Category("Fluent")] + [Category("Server")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + [NonParallelizable] + public sealed class InlineMethodIntegrationTests + { + private string m_pkiRoot; + private ServerFixture m_fixture; + private InlineMethodServer m_server; + private InlineMethodNodeManager m_manager; + private RequestHeader m_requestHeader; + private SecureChannelContext m_secureChannelContext; + + [OneTimeSetUp] + public async Task SetUpAsync() + { + m_pkiRoot = Path.Combine( + TestContext.CurrentContext.WorkDirectory, + nameof(InlineMethodIntegrationTests), + Guid.NewGuid().ToString("N")); + + m_fixture = new ServerFixture( + telemetry => new InlineMethodServer(telemetry)) + { + UriScheme = Utils.UriSchemeOpcTcp, + SecurityNone = true, + AutoAccept = true + }; + + m_server = await m_fixture.StartAsync(m_pkiRoot).ConfigureAwait(false); + m_manager = m_server.NodeManagerFactory.Manager; + (m_requestHeader, m_secureChannelContext) = await m_server + .CreateAndActivateSessionAsync(nameof(InlineMethodIntegrationTests)) + .ConfigureAwait(false); + } + + [OneTimeTearDown] + public async Task TearDownAsync() + { + if (m_requestHeader is not null) + { + m_requestHeader.Timestamp = DateTimeUtc.Now; + await m_server + .CloseSessionAsync( + m_secureChannelContext, + m_requestHeader, + true, + RequestLifetime.None) + .ConfigureAwait(false); + } + + m_server?.Dispose(); + if (m_fixture is not null) + { + await m_fixture.StopAsync().ConfigureAwait(false); + } + if (!string.IsNullOrEmpty(m_pkiRoot) && Directory.Exists(m_pkiRoot)) + { + Directory.Delete(m_pkiRoot, recursive: true); + } + } + + [Test] + public void InlineMethodIsBaseMethodStateWithArgumentProperties() + { + NodeState node = m_manager.Find( + ToNodeId(InlineMethodModel.MethodIds.Thermostat_Boost)); + + Assert.That(node, Is.TypeOf()); + var method = (MethodState)node; + Assert.That(method.InputArguments, Is.Not.Null); + Assert.That(method.OutputArguments, Is.Not.Null); + ArrayOf inputs = method.InputArguments.Value; + ArrayOf outputs = method.OutputArguments.Value; + Assert.Multiple(() => + { + Assert.That(inputs.Count, Is.EqualTo(1)); + Assert.That(inputs[0].Name, Is.EqualTo("Degrees")); + Assert.That(inputs[0].DataType, Is.EqualTo(DataTypeIds.Double)); + Assert.That(outputs.Count, Is.EqualTo(1)); + Assert.That(outputs[0].Name, Is.EqualTo("NewSetpoint")); + Assert.That(outputs[0].DataType, Is.EqualTo(DataTypeIds.Double)); + }); + } + + [Test] + public async Task InlineMethodCallReturnsHandlerResultAsync() + { + CallMethodResult result = await CallBoostAsync( + InlineMethodModel.MethodIds.Thermostat_Boost, + Variant.From(2.5)).ConfigureAwait(false); + + Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good)); + Assert.That(result.OutputArguments.Count, Is.EqualTo(1)); + Assert.That(result.OutputArguments[0].TryGetValue(out double newSetpoint), Is.True); + Assert.That(newSetpoint, Is.EqualTo(InlineMethodTestModel.InitialSetpoint + 2.5)); + } + + [Test] + public async Task TypeDeclarationMethodIdCallsInstanceHandlerAsync() + { + CallMethodResult result = await CallBoostAsync( + InlineMethodModel.MethodIds.ThermostatType_Boost, + Variant.From(1.0)).ConfigureAwait(false); + + Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.Good)); + Assert.That(result.OutputArguments[0].TryGetValue(out double newSetpoint), Is.True); + Assert.That(newSetpoint, Is.EqualTo(InlineMethodTestModel.InitialSetpoint + 1.0)); + } + + [Test] + public async Task InlineMethodCallRejectsWrongArgumentTypeAsync() + { + CallMethodResult result = await CallBoostAsync( + InlineMethodModel.MethodIds.Thermostat_Boost, + Variant.From("2.5")).ConfigureAwait(false); + + Assert.That(result.StatusCode, Is.EqualTo(StatusCodes.BadInvalidArgument)); + Assert.That(result.InputArgumentResults.Count, Is.EqualTo(1)); + Assert.That(result.InputArgumentResults[0], Is.EqualTo(StatusCodes.BadTypeMismatch)); + } + + private async Task CallBoostAsync( + ExpandedNodeId methodId, + Variant degrees) + { + m_requestHeader.Timestamp = DateTimeUtc.Now; + CallResponse response = await m_server.CallAsync( + m_secureChannelContext, + m_requestHeader, + [ + new CallMethodRequest + { + ObjectId = ToNodeId(InlineMethodModel.ObjectIds.Thermostat), + MethodId = ToNodeId(methodId), + InputArguments = [degrees] + } + ], + RequestLifetime.None).ConfigureAwait(false); + + Assert.That(response.ResponseHeader.ServiceResult, Is.EqualTo(StatusCodes.Good)); + Assert.That(response.Results.Count, Is.EqualTo(1)); + return response.Results[0]; + } + + private NodeId ToNodeId(ExpandedNodeId nodeId) + { + return ExpandedNodeId.ToNodeId(nodeId, m_server.CurrentInstance.NamespaceUris); + } + } +} diff --git a/tests/Opc.Ua.Server.Tests/Fluent/InlineMethodTestModel.cs b/tests/Opc.Ua.Server.Tests/Fluent/InlineMethodTestModel.cs new file mode 100644 index 0000000000..cfea9c98f2 --- /dev/null +++ b/tests/Opc.Ua.Server.Tests/Fluent/InlineMethodTestModel.cs @@ -0,0 +1,114 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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.Threading; +using System.Threading.Tasks; +using Opc.Ua; +using Opc.Ua.Server; +using Opc.Ua.Server.Fluent; +using Quickstarts.ReferenceServer; + +namespace InlineMethodModel +{ + /// + /// A source-generated node manager for a model whose method declares its + /// arguments inline, without a method type. + /// + [NodeManager(NamespaceUri = InlineMethodTestModel.NamespaceUri)] + public sealed partial class InlineMethodNodeManager + { + partial void Configure(IInlineMethodNodeManagerBuilder builder) + { + builder.Thermostat.Boost + .OnCall(degrees => InlineMethodTestModel.InitialSetpoint + degrees); + } + } + + /// + /// Captures the node manager instance the server creates so the tests can + /// assert against its address space. + /// + public sealed class InlineMethodNodeManagerCapturingFactory : + InlineMethodNodeManagerFactory + { + /// + /// The manager created for the running server. + /// + public InlineMethodNodeManager Manager { get; private set; } + + /// + public override ValueTask CreateAsync( + IServerInternal server, + ApplicationConfiguration configuration, + CancellationToken cancellationToken = default) + { + var manager = new InlineMethodNodeManager(server, configuration); + Manager = manager; + return new ValueTask(manager); + } + } + + /// + /// Constants shared by the inline method tests. + /// + public static class InlineMethodTestModel + { + /// + /// The model namespace generated from + /// Fluent/Assets/InlineMethod.xml. + /// + public const string NamespaceUri = "urn:opcfoundation.org:2026-09:InlineMethod"; + + /// + /// The setpoint the Boost handler adds its input to. + /// + public const double InitialSetpoint = 21.0; + } + + /// + /// A reference server that hosts the inline method node manager. + /// + public sealed class InlineMethodServer : ReferenceServer + { + /// + /// Initializes the server and registers the inline method node manager. + /// + public InlineMethodServer(ITelemetryContext telemetry) + : base(telemetry) + { + NodeManagerFactory = new InlineMethodNodeManagerCapturingFactory(); + AddNodeManager(NodeManagerFactory); + } + + /// + /// The factory which captured the created node manager. + /// + public InlineMethodNodeManagerCapturingFactory NodeManagerFactory { get; } + } +} diff --git a/tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj b/tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj index 04a180c435..784acaefce 100644 --- a/tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj +++ b/tests/Opc.Ua.Server.Tests/Opc.Ua.Server.Tests.csproj @@ -71,6 +71,13 @@ NodeSetImportModel NodeSetImportModel
+ + + urn:opcfoundation.org:2026-09:InlineMethod + InlineMethodModel + InlineMethodModel + diff --git a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs index 34705a0eaf..78e85ebe4d 100644 --- a/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs +++ b/tests/Opc.Ua.SourceGeneration.Core.Tests/Schema/ModelDesignExtensionsTests.cs @@ -3059,6 +3059,53 @@ namespace Opc.Ua.Schema.Model.Tests Assert.That(result, Is.EqualTo("TestMethodState")); } + /// + /// Tests GetClassName with a child method that declares its arguments inline. + /// Input: MethodDesign child of an ObjectType, no TypeDefinition, with arguments. + /// Expected: References use the base MethodState because no typed class is + /// generated; the declaration name keeps the typed class name. + /// + [Test] + public void GetClassName_InlineMethodWithArguments_ReferencesBaseMethodState() + { + // Arrange + var mockMethod = new MethodDesign + { + SymbolicId = new XmlQualifiedName("ThermostatType_Boost", "http://test.org"), + SymbolicName = new XmlQualifiedName("Boost", "http://test.org"), + TypeDefinition = null, + HasArguments = true, + InputArguments = + [ + new Parameter + { + Name = "Degrees", + DataType = new XmlQualifiedName("Double", "http://opcfoundation.org/UA/") + } + ] + }; + Namespace[] namespaces = []; + + // Act + string reference = mockMethod.GetNodeStateClassName("http://test.org", namespaces); + string factory = mockMethod.GetNodeStateClassName( + "http://test.org", + namespaces, + asFactory: true); + string declaration = mockMethod.GetNodeStateClassName( + "http://test.org", + namespaces, + applyStandardFallback: false); + + // Assert + Assert.Multiple(() => + { + Assert.That(reference, Is.EqualTo("global::Opc.Ua.MethodState")); + Assert.That(factory, Is.EqualTo("new global::Opc.Ua.MethodState")); + Assert.That(declaration, Is.EqualTo("BoostMethodState")); + }); + } + /// /// Tests GetClassName with a MethodDesign that has TypeDefinition not ending with Type suffixes and has arguments. /// Input: MethodDesign with TypeDefinition not ending in "Type" or "MethodType", HasArguments=true. diff --git a/tests/Opc.Ua.SourceGeneration.Tests/ModelDesignInlineMethodTests.cs b/tests/Opc.Ua.SourceGeneration.Tests/ModelDesignInlineMethodTests.cs new file mode 100644 index 0000000000..b80b503cee --- /dev/null +++ b/tests/Opc.Ua.SourceGeneration.Tests/ModelDesignInlineMethodTests.cs @@ -0,0 +1,224 @@ +/* ======================================================================== + * Copyright (c) 2005-2026 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.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using NUnit.Framework; + +namespace Opc.Ua.SourceGeneration +{ + /// + /// Tests ModelDesign methods whose arguments are declared on the method + /// itself rather than on a separate method type declaration. + /// + [TestFixture] + [Category("SourceGeneration")] + [SetCulture("en-us")] + [SetUICulture("en-us")] + public class ModelDesignInlineMethodTests + { + [Test] + public void InlineObjectTypeMethodWithArgumentsUsesBaseMethodState() + { + string generated = GenerateAndCompile( + ObjectType(InlineBoost) + Instance("ThermostatType")); + + Assert.Multiple(() => + { + Assert.That(generated, Does.Not.Contain("BoostMethodState")); + Assert.That(generated, Does.Contain("global::Opc.Ua.MethodState? Boost")); + Assert.That(generated, Does.Contain("state.CreateOrReplaceInputArguments(")); + Assert.That(generated, Does.Contain("state.CreateOrReplaceOutputArguments(")); + Assert.That(generated, Does.Contain("Name = \"Degrees\"")); + Assert.That(generated, Does.Contain("Name = \"NewSetpoint\"")); + }); + } + + [Test] + public void InlineObjectMethodWithArgumentsUsesBaseMethodState() + { + string generated = GenerateAndCompile( + Instance("ua:BaseObjectType", InlineBoost)); + + Assert.Multiple(() => + { + Assert.That(generated, Does.Not.Contain("BoostMethodState")); + Assert.That(generated, Does.Contain("state.CreateOrReplaceInputArguments(")); + Assert.That(generated, Does.Contain("Name = \"Degrees\"")); + }); + } + + [Test] + public void MethodTypeDeclarationKeepsTypedMethodState() + { + string generated = GenerateAndCompile( + BoostMethodType + + ObjectType( + """ + + """) + + Instance("ThermostatType")); + + Assert.Multiple(() => + { + Assert.That(generated, Does.Contain("class BoostMethodState")); + Assert.That(generated, Does.Contain("global::Heating.BoostMethodState? Boost")); + }); + } + + private static string GenerateAndCompile(string nodes) + { + CSharpCompilation compilation = OptimizationLevel.Release + .CreateCompilation() + .AddCode( + new Dictionary().WithOpcUaGeneratedStack(), + LanguageVersion.CSharp13); + var options = new AnalyzerOptionsProvider( + new Dictionary + { + ["build_property.ModelSourceGeneratorOmitFluentApi"] = "true" + }); + GeneratorDriver driver = CSharpGeneratorDriver.Create(new ModelSourceGenerator()) + .WithUpdatedParseOptions( + new CSharpParseOptions() + .WithKind(SourceCodeKind.Regular) + .WithLanguageVersion(LanguageVersion.CSharp13)) + .AddAdditionalTexts( + [EmbeddedText.Create("Heating.xml", ModelHeader + nodes + ModelFooter)]) + .WithUpdatedAnalyzerConfigOptions(options); + + driver = driver.RunGeneratorsAndUpdateCompilation( + compilation, + out Compilation outputCompilation, + out ImmutableArray generatorDiagnostics); + + Assert.That( + generatorDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), + Is.Empty); + Assert.That( + outputCompilation.GetDiagnostics() + .Where(d => d.Severity == DiagnosticSeverity.Error) + .Select(d => d.ToString()), + Is.Empty); + + return string.Join( + "\n", + driver.GetRunResult().Results + .SelectMany(result => result.GeneratedSources) + .Select(source => source.SourceText.ToString())); + } + + private static string ObjectType(string method) + { + return + $""" + + + {method} + + + """; + } + + private static string Instance(string typeDefinition, string method = null) + { + string children = method == null + ? string.Empty + : $"{method}"; + return + $""" + + {children} + + + ua:Organizes + ua:ObjectsFolder + + + + """; + } + + private const string InlineBoost = + """ + + + + + + + + + """; + + private const string BoostMethodType = + """ + + + + + + + + + """; + + private const string ModelHeader = + """ + + + + http://example.com/Heating/ + http://opcfoundation.org/UA/ + + + """; + + private const string ModelFooter = + """ + + + """; + } +} diff --git a/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs b/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs index 5f1e2c9d29..ce5672a076 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Generators/MethodDesignArgumentResolver.cs @@ -146,6 +146,40 @@ namespace Opc.Ua.SourceGeneration method.SymbolicId; } + /// + /// Returns whether the method state of a method is a typed class that + /// is declared by a method type, rather than the base + /// MethodState. + /// + /// + /// Typed method-state classes are only generated for method type + /// declarations. A method that declares its own arguments inline, + /// without a method type or a declaration that resolves to one, has + /// no typed class; its argument properties are still generated. + /// + public static bool HasTypedMethodState(MethodDesign method) + { + if (method == null) + { + throw new ArgumentNullException(nameof(method)); + } + + if (method.TypeDefinition != null && method.MethodType == null) + { + return true; + } + + MethodDesign definition = ResolveMethodDefinition(method); + if (definition.TypeDefinition != null || definition.IsMethodTypeDesign()) + { + return true; + } + + XmlQualifiedName identity = definition.SymbolicName ?? definition.SymbolicId; + return identity?.Name != null && + identity.Name.EndsWith("MethodType", StringComparison.Ordinal); + } + /// /// Returns whether two methods have identical effective signatures. /// diff --git a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs index 66468365a6..50b7fbded3 100644 --- a/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs +++ b/tools/Opc.Ua.SourceGeneration.Core/Schema/ModelDesignExtensions.cs @@ -177,6 +177,17 @@ namespace Opc.Ua.Schema.Model if (MethodDesignArgumentResolver.HasMethodArguments(method)) { + // Only method types have a typed class. A reference to a + // method that declares its arguments inline uses the base + // MethodState; declaration names keep the typed name. + if (applyStandardFallback && + !MethodDesignArgumentResolver.HasTypedMethodState(method)) + { + return CoreUtils.Format( + "{0}global::Opc.Ua.MethodState", + asFactory ? "new " : string.Empty); + } + string typedClassName = CoreUtils.Format( "{0}{1}MethodState", asFactory ? "new " : string.Empty,