mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-28 22:55:49 +08:00
# Description Remove stock phrasing and correct misleading or inconsistent technical documentation. The initial two passes covered 367 Markdown files, followed by an interactive review of 169 editorial flags. The U5 interpretation originally deferred in that review has since received the upstream Mantis answer recorded by #4843; remaining server/spec follow-ups stay explicit in the CTT log. - Correct package responsibilities, migration steps, API names, ownership, protocol descriptions and links. Keep bundled migration guides synchronized. Preserve historical conclusions as historical records with separate re-check notes. - Provide at least two rendered TOC levels throughout `docs/`. Convert appropriate architecture figures to Mermaid without changing code examples, transcripts or historical quotations. - Address the October 19 feedback with concrete setup/migration instructions, corrected certificate paths and PubSub names, shorter repeated caveats, consistent XML comments, and a full review of the Concepts guide. Retain #4839's API corrections. - Integrate #4841's runtime/performance guidance and review incoming/recent prose. Preserve dated measurements rather than claiming new benchmark runs; clarify allocation ratios, startup-only processor settings, JIT warm-up and the benchmark commit's scope. Missing raw measurement artifacts and repeat ranges remain tracked by #4842. - Merge master through `e8c5d800a` in `32c6394b7`. Resolve the CTT log conflict using the updated Mantis states and OPC answers, retaining historical framing and re-checks for C28/C32/C43. - Address the latest naming/index feedback in `adb5bf693`: rename the guides to `docs/WotCon.md` and `docs/WotNodeSetConversion.md`; move the provider and DataType-declaration contracts into the conversion guide and remove `docs/WoT.md`. Rename `samples/DI` to `samples/Di` and `samples/Isa95` to `samples/ISA95`. Add content-index READMEs to Di, FluentApi, ISA95, Lds and PubSub; update solution entries, project references, Docker/CI paths, test fixture paths and documentation links with exact casing. **WoT guide scope.** This branch retains the detailed conversion guide that #4758 had replaced with an anchor stub. Conversion limits, verified document sets, preservation rules and provider ownership now live together in `WotNodeSetConversion.md`; binding/runtime guidance remains in `WotBindings.md` and `WotCon.md`. The design-rationale/test-ledger prose was shortened, the oversized argument-table cell moved into its section, and existing anchors preserved. **SDK policy is a separate, explicit user-directed change.** `global.json` sets a stable minimum of `10.0.400`, `rollForward: latestMajor`, and `allowPrerelease: false`. Newer major SDKs are permitted; CI's explicit SDK pins are unchanged. README, GettingStarted, DeveloperGuide and Recycling instructions reflect the possible analyzer differences. The image-contract regression fix accepts compatible stable SDK images instead of requiring exact equality with the floor. The latest four rename/index threads are resolved after publication. Three older threads remain open: universal obsolete-removal timing, unconditional nonce-strength wording, and a blanket Vision NativeAOT claim. Existing implementation/evidence constraints are explained in their inline replies. The two-level TOCs remain as requested. ## Validation - `git diff --check` and staged diff checks: passed. The untouched upstream AutoID NodeSet retains its provenance bytes, including two existing license-header trailing spaces. - `.\.azurepipelines\validate-migration-plugin.ps1`: passed; 15 bundled migration documents and manifests synchronized. - Documentation/path validation: **101** documents retain two rendered TOC levels (**2,228** entries), with idempotent formatting. **52** new relative links and their anchors passed exact-case checks. No tracked references to the old guide/sample paths remain. Both relocated provider-contract sections are preserved in the destination. - `dotnet sln UA.slnx list`: passed; shows the renamed Di/ISA95 projects and the existing FluentApi/Lds/PubSub projects. Updated solution and pump ProjectReference paths also passed exact-case existence checks. - Latest focused commands, run from the repository root: ```powershell # Passed: 61 channel send-order and reconnect tests. dotnet test tests\Opc.Ua.Core.Tests\Opc.Ua.Core.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~UaSCBinaryChannelSendOrderTests|FullyQualifiedName~TcpReconnectKeyAgreementTests' --verbosity minimal # Passed: 5 pump datasheet tests; builds the renamed pump sample. dotnet test tests\Opc.Ua.Di.Tests\Opc.Ua.Di.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~PumpDatasheetConformanceTests' --verbosity minimal # Passed: 4 repository NodeSet path/round-trip tests, including the moved pump models. dotnet test tests\Opc.Ua.Types.Tests\Opc.Ua.Types.Tests.csproj -p:CustomTestTarget=net10.0 --filter 'FullyQualifiedName~RepositorySourceSurvivesSerializedLinkedPartitionsAsync' --verbosity minimal # Passed: build, zero warnings/errors. dotnet build samples\ISA95\MinimalIsa95Server\MinimalIsa95Server.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal # Passed: dependent sample-test project build, zero warnings/errors. dotnet build tests\Opc.Ua.WotCon.Samples.Tests\Opc.Ua.WotCon.Samples.Tests.csproj -p:CustomTestTarget=net10.0 -f net10.0 --verbosity minimal ``` Earlier focused integration checks passed 2,620 tests across AutoID, event queues/scheduling, source generation, MCP/package profiles and WoT. Both reviewed Mermaid diagrams rendered locally without network access. These checks are not a full solution run. The full solution has **not** been rerun for this review. Earlier `dotnet test UA.slnx -p:CustomTestTarget=net10.0 --verbosity quiet` did not pass: three AOT projects rejected VSTest and 20 Vision tests failed during graphics initialization with `OutOfMemoryException`. Remaining long-running tests were stopped at the user's request. A focused Vision rerun passed 558 tests and reproduced those 20 failures. The earlier `dotnet test UA.slnx -p:CustomTestTarget=net48 --verbosity quiet` was blocked by the former SDK pin and was not retried after the policy change. The user approved proceeding with those limitations documented; current remote CI is not claimed green. ## Related Issues - Follow-up #4842 tracks the requested 2.0.2+ benchmark/tuning and diagnostic-tool documentation refresh. #4841 supplies documentation and earlier results, not all newly requested measurements. - #4758 explains the conversion guide's earlier consolidation; the current provider contracts are now consolidated into `WotNodeSetConversion.md`. - #4839 supplies the reviewed API corrections; #4843 supplies the updated CTT issue record. - The original prose cleanup and SDK policy were user-requested without a source issue. No new runtime architecture is introduced by these documentation edits. ## Checklist _Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._ - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [ ] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [ ] I have verified that my changes do not introduce (new) build or analyzer warnings. - [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Roman Ettlinger <romanett98@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
441 lines
20 KiB
Markdown
441 lines
20 KiB
Markdown
# OPC UA over OpenAPI
|
|
|
|
This guide shows how to let an OPC UA server built with this stack describe
|
|
its REST binding as an OpenAPI document, and how to talk to the server from
|
|
generated REST clients. It is task-oriented: serve the document, generate a
|
|
client, get the JSON right, and avoid the traps of generated clients.
|
|
|
|
The binding itself (routes, encoding negotiation, discovery, WebSocket
|
|
sub-profile) is described in the [REST binding reference](WebApi.md).
|
|
|
|
## Contents
|
|
|
|
- [When to use the REST binding](#when-to-use-the-rest-binding)
|
|
- [The document is generated](#the-document-is-generated)
|
|
- [Serving the document](#serving-the-document)
|
|
- [Sessionless only](#sessionless-only)
|
|
- [Composing your own pipeline](#composing-your-own-pipeline)
|
|
- [Generating the document without a server](#generating-the-document-without-a-server)
|
|
- [Generating a client](#generating-a-client)
|
|
- [Calling the server](#calling-the-server)
|
|
- [A generated .NET client](#a-generated-net-client)
|
|
- [Sessions over REST](#sessions-over-rest)
|
|
- [.NET clients without code generation](#net-clients-without-code-generation)
|
|
- [Wire format cheat sheet](#wire-format-cheat-sheet)
|
|
- [Known issues with generated clients](#known-issues-with-generated-clients)
|
|
- [DataValue status and the published documents](#datavalue-status-and-the-published-documents)
|
|
- [Validating responses against the published schemas](#validating-responses-against-the-published-schemas)
|
|
- [Polymorphic structures](#polymorphic-structures)
|
|
- [How the document is kept correct](#how-the-document-is-kept-correct)
|
|
- [Troubleshooting](#troubleshooting)
|
|
|
|
## When to use the REST binding
|
|
|
|
| You have | Use |
|
|
| --- | --- |
|
|
| A .NET application that can reference this stack | `ManagedSession` over `opc.tcp`, or over REST with `UseWebApiEndpoint` if only HTTPS gets through the network |
|
|
| A web front end, a script, a low-code platform, a language without an OPC UA SDK | The REST binding with a client generated from the OpenAPI document |
|
|
| Subscriptions, events, browsing large address spaces | A session. Over REST this means long-polled `/publish`; `opc.tcp` is cheaper |
|
|
|
|
The REST binding runs on the same Kestrel listener as the HTTPS binary and
|
|
`opcua+uajson` transports: one port, no extra process.
|
|
|
|
## The document is generated
|
|
|
|
The OPC Foundation publishes the mapping of the OPC UA services to
|
|
OpenAPI 3.0 in
|
|
[`UA-Nodeset/OpenApi`](https://github.com/OPCFoundation/UA-Nodeset/tree/latest/OpenApi),
|
|
produced by the model compiler. This stack does not ship those files. The
|
|
routes in `WebApiServiceRoutes` and the structure definitions of the
|
|
request and response types are the source of truth for what the binding
|
|
accepts and sends, so the document is generated from them at runtime by
|
|
`WebApiOpenApiGenerator` (package `Opc.Ua.Core.Schema`). A test compares the
|
|
generated document with the published documents
|
|
(see [How the document is kept correct](#how-the-document-is-kept-correct)).
|
|
|
|
Two levels of detail keep the size in check. The published documents are
|
|
about 300 KB for the two service sets; the generated ones are:
|
|
|
|
| Level | Option | Content | Size (all services) |
|
|
| --- | --- | --- | --- |
|
|
| Operations | default | Paths, `operationId`s, and a JSON object as every request and response body | about 12 KB |
|
|
| Schemas | `OpenApiIncludeSchemas = true` | Additionally one component schema per request, response and contained structure or enumeration (100 for all services) | about 46 KB |
|
|
|
|
Use *Operations* to document and explore the API, for example in an API
|
|
portal. Use *Schemas* as the input of an OpenAPI client generator that emits
|
|
typed models.
|
|
|
|
| `WebApiServiceSet` | Services |
|
|
| --- | --- |
|
|
| `AllServices` | All 28: Discovery, Session, View, Attribute, Method, MonitoredItem, Subscription |
|
|
| `Sessionless` | Read, Write, HistoryRead, HistoryUpdate, Call, Browse, BrowseNext, TranslateBrowsePathsToNodeIds |
|
|
|
|
Every service is a `POST` to a lower-case path (`/read`, `/browse`,
|
|
`/createsubscription`, ...). The body is the bare `<Service>Request` and the
|
|
answer is the bare `<Service>Response`, with no envelope. NodeManagement and
|
|
Query are not part of either set.
|
|
|
|
## Serving the document
|
|
|
|
The binding does not serve the document unless you ask for it. Set
|
|
`OpenApiDocumentPath`:
|
|
|
|
```csharp
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Opc.Ua.Bindings;
|
|
using Opc.Ua.Bindings.WebApi;
|
|
|
|
services.AddOpcUa()
|
|
.AddHttpsTransport()
|
|
.AddWebApiTransport(opt =>
|
|
{
|
|
opt.ServiceSet = WebApiServiceSet.AllServices; // default
|
|
opt.OpenApiDocumentPath = "/openapi.json"; // default null: not served
|
|
opt.OpenApiIncludeSchemas = true; // default false
|
|
});
|
|
```
|
|
|
|
The server needs an `https://` base address; the
|
|
[Transports guide](Transports.md) shows how to configure one.
|
|
|
|
What the route does:
|
|
|
|
- **It is one of the REST routes.** It is mapped in the same route group as
|
|
the service routes and needs whatever they need. When an authentication
|
|
scheme is registered (`AddWebApiBearerAuth`, `AddWebApiBasicAuth`, ...),
|
|
`GET /openapi.json` requires an authenticated caller like `POST /read`.
|
|
Only `/findservers` and `/getendpoints` are anonymous. The document is
|
|
generated from the routes the host maps and reveals nothing of the address
|
|
space, but it is not published to callers the service routes would turn
|
|
away.
|
|
- **`servers` follows the path base.** Behind `UsePathBase("/opcua")` the
|
|
document lists the relative server URL `/opcua/`, which OpenAPI 3.0
|
|
clients resolve against the address they loaded the document from. Without
|
|
a path base the list is omitted and OpenAPI assumes `/`.
|
|
- **The document is generated once** per server URL and kept in memory.
|
|
|
|
### Sessionless only
|
|
|
|
```csharp
|
|
.AddWebApiTransport(opt =>
|
|
{
|
|
opt.ServiceSet = WebApiServiceSet.Sessionless;
|
|
opt.OpenApiDocumentPath = "/openapi.json";
|
|
})
|
|
```
|
|
|
|
Only the eight sessionless routes are mapped, and the document describes
|
|
exactly those, so a generated client contains the operations the binding
|
|
maps. Discovery and session management remain available on the binary and
|
|
`opcua+uajson` endpoints of the same listener, for clients that need them.
|
|
|
|
The service set only limits the routes. Whether the server answers a request
|
|
that carries no session (OPC 10000-4 6.3) is decided by the server's session
|
|
manager: without a `ValidateSessionLessRequest` handler on `ISessionManager`,
|
|
such a request is answered with `Bad_SessionIdInvalid`.
|
|
|
|
### Composing your own pipeline
|
|
|
|
Hosts that build their own ASP.NET Core pipeline instead of using
|
|
`AddWebApiTransport` map the same endpoints directly and apply their own
|
|
conventions to the group:
|
|
|
|
```csharp
|
|
app.UseRouting();
|
|
app.UseAuthentication();
|
|
app.UseAuthorization();
|
|
app.UseEndpoints(endpoints =>
|
|
{
|
|
endpoints
|
|
.MapWebApiEndpoints(new WebApiTransportOptions
|
|
{
|
|
ServiceSet = WebApiServiceSet.Sessionless,
|
|
OpenApiDocumentPath = "/spec/opcua.json"
|
|
})
|
|
.RequireAuthorization();
|
|
});
|
|
```
|
|
|
|
The generator that builds the document is taken from the request services
|
|
(`WebApiOpenApiGenerator`, registered by `AddWebApiTransport`); a default
|
|
generator is used when none is registered.
|
|
|
|
## Generating the document without a server
|
|
|
|
`WebApiOpenApiGenerator` returns the document as a `JsonObject`, for example
|
|
to publish it to an API portal in a build step:
|
|
|
|
```csharp
|
|
using System.Text.Json.Nodes;
|
|
using Opc.Ua.Bindings;
|
|
using Opc.Ua.Schema.OpenApi;
|
|
|
|
var generator = new WebApiOpenApiGenerator();
|
|
JsonObject document = generator.Generate(
|
|
WebApiServiceSet.Sessionless,
|
|
includeSchemas: true,
|
|
serverUrl: "https://plant.example.com/opcua/");
|
|
File.WriteAllText("opcua-sessionless.json", document.ToJsonString());
|
|
```
|
|
|
|
The generator reads the structure definitions from the encodeable types of
|
|
the OPC UA namespace. Pass an `IDataTypeDefinitionResolver` to the
|
|
constructor to describe other types. The generator builds the document with
|
|
`System.Text.Json.Nodes` and no reflection, so it is NativeAOT compatible
|
|
(see [Runtime schema generation](SchemaGeneration.md)).
|
|
|
|
## Generating a client
|
|
|
|
Point any OpenAPI 3.0 generator at the running server. Enable
|
|
`OpenApiIncludeSchemas` first, otherwise every body is an untyped object:
|
|
|
|
```bash
|
|
# .NET
|
|
openapi-generator-cli generate -g csharp \
|
|
-i https://server:4843/openapi.json -o ./opcua-client \
|
|
--additional-properties=packageName=Plant.OpcUa.Rest,targetFramework=net8.0
|
|
|
|
# TypeScript (fetch)
|
|
openapi-generator-cli generate -g typescript-fetch \
|
|
-i https://server:4843/openapi.json -o ./opcua-client-ts
|
|
|
|
# Python
|
|
openapi-generator-cli generate -g python \
|
|
-i https://server:4843/openapi.json -o ./opcua-client-py
|
|
```
|
|
|
|
When the server requires authentication, download the document with the
|
|
credentials the service routes accept and generate from the file.
|
|
|
|
The OPC Foundation publishes ready-generated clients built from its own
|
|
documents, so you can skip generation entirely:
|
|
|
|
| Language | Repository |
|
|
| --- | --- |
|
|
| .NET | [opcua-webapi-dotnet](https://github.com/OPCFoundation/opcua-webapi-dotnet) |
|
|
| Python | [opcua-webapi-python](https://github.com/OPCFoundation/opcua-webapi-python) |
|
|
| TypeScript | [opcua-webapi-typescript](https://github.com/OPCFoundation/opcua-webapi-typescript) |
|
|
|
|
A client generated from the sessionless document has eight operations; one
|
|
generated from the all-services document has 28. Generate against the
|
|
document your server actually serves.
|
|
|
|
## Calling the server
|
|
|
|
### A generated .NET client
|
|
|
|
The names below are those of the OPC Foundation .NET client; a client you
|
|
generate from the server's document has the same operations and models.
|
|
|
|
```csharp
|
|
using Opc.Ua.WebApi.Api;
|
|
using Opc.Ua.WebApi.Client;
|
|
using Opc.Ua.WebApi.Model;
|
|
|
|
var api = new DefaultApi(new Configuration { BasePath = "https://server:4843" });
|
|
|
|
ReadResponse response = await api.ReadAsync(new ReadRequest
|
|
{
|
|
RequestHeader = new RequestHeader
|
|
{
|
|
AuthenticationToken = sessionToken, // from /createsession, see below
|
|
TimeoutHint = 10000
|
|
},
|
|
TimestampsToReturn = 2, // Both
|
|
NodesToRead =
|
|
[
|
|
new ReadValueId { NodeId = "ns=2;s=Pump01.Measurements.Flow", AttributeId = 13 }
|
|
]
|
|
});
|
|
|
|
DataValue flow = response.Results[0];
|
|
Console.WriteLine($"{flow.Value} (UaType {flow.UaType})");
|
|
```
|
|
|
|
Set the base path explicitly. Before relying on the status of a value, read
|
|
[the DataValue status issue](#datavalue-status-and-the-published-documents)
|
|
below.
|
|
|
|
### Sessions over REST
|
|
|
|
The session services work like their binary counterparts; the session is
|
|
identified by the `AuthenticationToken` in every `RequestHeader`:
|
|
|
|
1. `POST /createsession` and keep `AuthenticationToken` from the response.
|
|
2. `POST /activatesession` with that token and a `UserIdentityToken`.
|
|
3. Every further request carries
|
|
`"RequestHeader": { "AuthenticationToken": "<token>" }`.
|
|
4. `POST /closesession` when done.
|
|
|
|
REST requests run against the REST endpoint the server announces for its
|
|
`https://` base address (`TransportProfileUri`
|
|
`http://opcfoundation.org/UA-Profile/Transport/https-uajson-openapi`,
|
|
OPC 10000-6 7.4.1); `/getendpoints` lists it with its user token policies.
|
|
|
|
For subscriptions, call `/createsubscription` and `/createmonitoreditems`,
|
|
then keep one `/publish` request outstanding. The server holds each
|
|
`/publish` request open until notifications arrive or the request's
|
|
`TimeoutHint` expires. Your HTTP client's timeout must be longer than that
|
|
hint (see [Long-poll `/publish`](WebApi.md#long-poll-publish)).
|
|
|
|
## .NET clients without code generation
|
|
|
|
If the client can reference this stack, you do not need a generated client:
|
|
|
|
| API | When |
|
|
| --- | --- |
|
|
| `ManagedSessionBuilder.UseWebApiEndpoint(url)` | You want the full `ISession`: reconnect, subscriptions, the typed companion-spec clients, all over REST |
|
|
| `WebApiClient` (`IWebApiClient`) | You want plain request/response calls with the stack's own request types and no session management |
|
|
|
|
```csharp
|
|
using Opc.Ua;
|
|
using Opc.Ua.Client.WebApi;
|
|
|
|
using var http = new HttpClient { BaseAddress = new Uri("https://server:4843/") };
|
|
using var client = new WebApiClient(http, new WebApiClientOptions
|
|
{
|
|
Encoding = WebApiEncoding.Compact
|
|
});
|
|
|
|
ReadResponse response = await client.ReadAsync(new ReadRequest
|
|
{
|
|
RequestHeader = new RequestHeader { AuthenticationToken = sessionToken },
|
|
NodesToRead = new ReadValueId[]
|
|
{
|
|
new() { NodeId = VariableIds.Server_ServerStatus_CurrentTime, AttributeId = Attributes.Value }
|
|
}
|
|
});
|
|
```
|
|
|
|
Both use the stack's own encoder and are therefore not affected by the
|
|
[known issues](#known-issues-with-generated-clients) below.
|
|
|
|
## Wire format cheat sheet
|
|
|
|
Useful when writing requests by hand or debugging a generated client. The
|
|
full rules are in [Part 6 5.4](https://reference.opcfoundation.org/Core/Part6/v105/docs/5.4).
|
|
|
|
| OPC UA type | JSON | Example |
|
|
| --- | --- | --- |
|
|
| NodeId | String | `"i=2258"`, `"ns=2;s=Pump01"`, `"nsu=http://opcfoundation.org/UA/;i=2258"` |
|
|
| QualifiedName | String | `"2:Flow"` |
|
|
| Enumeration | Integer | `"TimestampsToReturn": 2` |
|
|
| DateTime | ISO 8601 string | `"2026-09-23T10:15:30.1234567+02:00"` |
|
|
| Int64, UInt64 | String | `"9007199254740993"` |
|
|
| StatusCode | Object, omitted when Good | `{ "Code": 2150891520 }` (Verbose adds `"Symbol"`) |
|
|
| LocalizedText | Object | `{ "Locale": "en", "Text": "Pump" }` |
|
|
| Variant | `UaType` + `Value` (+ `Dimensions` for matrices) | `{ "UaType": 6, "Value": 42 }`, `{ "UaType": 11, "Value": [1.5, 2.5] }` |
|
|
| DataValue | The Variant's fields plus `Status` and timestamps | `{ "UaType": 6, "Value": 42, "SourceTimestamp": "..." }` |
|
|
| ExtensionObject | `UaTypeId` + body | see below |
|
|
|
|
Common `UaType` values: 1 Boolean, 6 Int32, 7 UInt32, 10 Float, 11 Double,
|
|
12 String, 13 DateTime, 17 NodeId, 21 LocalizedText, 22 ExtensionObject.
|
|
|
|
**Omitted fields decode as their default.** A request only needs the fields
|
|
it sets; `"MaxAge"`, `"RequestHandle"` and friends can be left out.
|
|
|
|
**Encoding flavour.** The server answers in Compact JSON unless the request
|
|
asks for Verbose (`Content-Type` or `Accept: application/json;
|
|
encoding=verbose`). Verbose keeps default values and adds status symbols,
|
|
which helps when debugging. The generated document describes the Compact
|
|
encoding.
|
|
|
|
**ExtensionObject bodies** come in two forms. A client built with this
|
|
stack sends the structure's fields inline next to `UaTypeId`. Generated
|
|
clients cannot, because the `ExtensionObject` schema only has `UaTypeId`,
|
|
`UaEncoding` and `UaBody`, so they send the OPC UA *binary* encoding, base64
|
|
in `UaBody`, with `UaEncoding: 1`. The server accepts both. An anonymous
|
|
identity token for `/activatesession`, as a generated client sends it:
|
|
|
|
```json
|
|
"UserIdentityToken": {
|
|
"UaTypeId": "i=321",
|
|
"UaEncoding": 1,
|
|
"UaBody": "CQAAAGFub255bW91cw=="
|
|
}
|
|
```
|
|
|
|
`i=321` is `AnonymousIdentityToken_Encoding_DefaultBinary`; the body is the
|
|
binary-encoded `PolicyId` `"anonymous"` (Int32 length, then UTF-8).
|
|
|
|
## Known issues with generated clients
|
|
|
|
### DataValue status and the published documents
|
|
|
|
Part 6 [Table 42](https://reference.opcfoundation.org/Core/Part6/v105/docs/5.4.2.18)
|
|
names the DataValue status field `"Status"`. The documents the OPC
|
|
Foundation publishes name it `"StatusCode"` (reported as
|
|
[UA-Nodeset#146](https://github.com/OPCFoundation/UA-Nodeset/issues/146)).
|
|
The stack follows the specification text: its encoder writes `"Status"`,
|
|
its decoder reads `"Status"` only, and the generated document names the
|
|
field `Status`.
|
|
|
|
| Client generated from | Effect |
|
|
| --- | --- |
|
|
| This server's document | Reads and writes the status correctly |
|
|
| The published OPC Foundation document, or a ready-generated client of the OPC Foundation | Ignores `"Status"` in responses and reports **Good** for bad and uncertain values; the status it sends in a `Write` is ignored by the server |
|
|
|
|
Until the publication and the specification text agree, generate clients
|
|
from the server's own document, or read `Status` from the raw response.
|
|
The stack's own clients (`ManagedSession`, `WebApiClient`) are unaffected.
|
|
|
|
### Validating responses against the published schemas
|
|
|
|
The published `DataValue` schema sets `"additionalProperties": false`. A
|
|
validator that enforces it rejects every DataValue with a non-Good status
|
|
that the stack sends, for the same reason. The generated schema names the
|
|
field `Status` and validates it.
|
|
|
|
### Polymorphic structures
|
|
|
|
Filters (`DataChangeFilter`, `EventFilter`), notifications and identity
|
|
tokens are ExtensionObjects. Generated clients only see the
|
|
`UaTypeId`/`UaEncoding`/`UaBody` envelope, so they have to produce and
|
|
consume the binary encoding (see the
|
|
[cheat sheet](#wire-format-cheat-sheet)). If the application needs event
|
|
filters or subscriptions, a .NET client using
|
|
[`UseWebApiEndpoint`](#net-clients-without-code-generation) is considerably
|
|
less work.
|
|
|
|
## How the document is kept correct
|
|
|
|
`WebApiOpenApiConformanceTests` (in `tests/Opc.Ua.Core.Schema.Tests/OpenApi/`)
|
|
compares the generated document with the two published documents, which are
|
|
embedded in the test assembly only (see the README next to them for the
|
|
source commit). Everything both describe has to be equal:
|
|
|
|
- the paths, `operationId`s and the request and response schema of every
|
|
operation, for both service sets;
|
|
- the set of component schemas the operations reach (100 for all services);
|
|
- every enumeration with its members and values;
|
|
- for every structure the property names, types, formats, integer ranges and
|
|
inheritance.
|
|
|
|
The differences that remain are listed in the test with their reason, and
|
|
the test fails when a listed difference disappears or a new one appears:
|
|
|
|
| Where | Generated | Published | Reason |
|
|
| --- | --- | --- | --- |
|
|
| `DataValue` status property | `Status` | `StatusCode` | Part 6 Table 42; UA-Nodeset#146 |
|
|
| `description`, `default` keywords | not written | specification links, .NET defaults | Documentation and defaults, not part of the contract; the Compact encoding omits default values |
|
|
|
|
`WebApiOpenApiWireFormatTests` validates messages the binding actually
|
|
writes (every request and response, plus populated samples with variants,
|
|
status codes, references and extension objects) against the generated
|
|
component schemas, so the schemas describe the wire format and not only the
|
|
names.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Cause | Fix |
|
|
| --- | --- | --- |
|
|
| `GET /openapi.json` returns 404 | `OpenApiDocumentPath` is not set (the default) or differs, or the REST binding isn't registered | Set `OpenApiDocumentPath` in `AddWebApiTransport` |
|
|
| `GET /openapi.json` returns 401 | An authentication scheme is registered; the document requires the credentials the service routes require | Send the credentials |
|
|
| Generated client has untyped request and response bodies | The document was served without schemas | Set `OpenApiIncludeSchemas = true` |
|
|
| Swagger UI "Try it out" goes to the wrong host behind a proxy | The relative server URL is resolved against the proxy's internal address | Serve the document behind the same path base the clients use, or edit `servers` in a generated copy |
|
|
| `/createsession` returns an HTTP error, `/read` works | `ServiceSet = Sessionless`: the route is not mapped and the request falls through to the listener's binary/JSON handler | Use `AllServices`, or the binary endpoint for sessions |
|
|
| HTTP 400, empty body | The body is not valid OPC UA JSON for that request (misspelled property, wrong type, NodeId not parseable) | Compare with the [cheat sheet](#wire-format-cheat-sheet); the server logs the decode error at Information level |
|
|
| `ServiceResult` `BadSessionIdInvalid` | Request without, or with a stale, `AuthenticationToken` | Create and activate a session first; send its token in every `RequestHeader` |
|
|
| Bad values show up as Good in a generated client | [Status vs StatusCode](#datavalue-status-and-the-published-documents) | Generate the client from the server's document |
|
|
| `/publish` times out on the client (`BadTimeout`) | HTTP client timeout shorter than `TimeoutHint` | Raise the client timeout above the hint |
|