Files
5899354441 Clarify documentation, migration guidance, and SDK selection (#4823)
# 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>
2026-10-21 10:10:55 +02:00

296 lines
30 KiB
Markdown

# OPC UA ISA-95 (OPC-10030 / OPC-10031-4)
End-to-end developer guide for the `Opc.Ua.ISA95*` library trio: the OPC-10030 ISA-95 Common Model and the OPC-10031-4 Job Control V1 and V2 companion models.
## Contents
- [Library layout](#library-layout)
- [Models and namespaces](#models-and-namespaces)
- [Quick start — server](#quick-start--server)
- [Quick start — client](#quick-start--client)
- [Server hosting model](#server-hosting-model)
- [Common-model builder reference](#common-model-builder-reference)
- [Job Control (V1 and V2)](#job-control-v1-and-v2)
- [GeoSpatialLocationType provider seam](#geospatiallocationtype-provider-seam)
- [Client](#client)
- [In-memory limitations and HA guidance](#in-memory-limitations-and-ha-guidance)
- [Conformance matrix](#conformance-matrix)
- [Model sources, identifier mappings, and normative repairs](#model-sources-identifier-mappings-and-normative-repairs)
- [Provenance](#provenance)
- [Normative Common Model repairs](#normative-common-model-repairs)
## Library layout
| Library | Role |
|---------|------|
| `Opc.Ua.ISA95` | Model assembly: source-generated NodeId tables, DataTypes, and ObjectType client proxies for the Common Model and both Job Control versions. Shared type contract for the client and server packages. |
| `Opc.Ua.ISA95.Server` | Server: `Isa95NodeManager` (one multi-namespace `FluentNodeManagerBase`), the typed common-model builder, Job Control provider abstractions plus an in-memory implementation, the `GeoSpatialLocationType` provider seam, and `AddIsa95Server` hosting integration. |
| `Opc.Ua.ISA95.Client` | Client: `Isa95Client` discovery entry point, direct V1/V2 Job Control clients, typed V2 status-event streaming, and `AddIsa95Client` hosting integration. |
The running example is [`samples/ISA95/MinimalIsa95Server`](../samples/ISA95/MinimalIsa95Server), a minimal server hosting all three namespaces with a seeded demo job order.
## Models and namespaces
| Model | Spec | ModelUri | Version | Publication date | C# namespace |
|-------|------|----------|---------|-------------------|---------------|
| Common Model | OPC-10030 | `http://www.OPCFoundation.org/UA/2013/01/ISA95` | 1.00 | 2013-11-06 | `Opc.Ua.ISA95` |
| Job Control V1 | OPC-10031-4 | `http://opcfoundation.org/UA/ISA95-JOBCONTROL` | 1.0.0 | 2021-03-31 | `Opc.Ua.ISA95.JobControl.V1` |
| Job Control V2 | OPC-10031-4 | `http://opcfoundation.org/UA/ISA95-JOBCONTROL_V2/` | 2.0.0 | 2024-01-31 | `Opc.Ua.ISA95.JobControl.V2` |
## Quick start — server
```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Opc.Ua.ISA95;
using Opc.Ua.ISA95.Server.Builders;
using Opc.Ua.ISA95.Server.Providers;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddOpcUa()
.AddServer(options =>
{
options.ApplicationName = "MyIsa95Server";
options.ApplicationUri = "urn:localhost:OPCFoundation:MyIsa95Server";
options.EndpointUrls.Add("opc.tcp://localhost:62545/MyIsa95Server");
})
.AddIsa95Server()
.ConfigureModel(async (model, ct) =>
{
EquipmentClassState reactorClass =
await model.CreateEquipmentClassAsync(model.Root, "ReactorClass", ct)
.ConfigureAwait(false);
EquipmentState reactor =
await model.CreateEquipmentAsync(model.Root, "Reactor-1", ct)
.ConfigureAwait(false);
model.DefinedByEquipmentClass(reactor, reactorClass);
});
await builder.Build().RunAsync();
```
`AddIsa95Server` registers the default in-memory Job Control provider (V1 and V2) only when no Job Control provider facet is already registered, plus the `Isa95NodeManagerFactory` and `Isa95ServerOptions`.
`ConfigureModel` runs after the ISA-95 root folder is created but before the server accepts connections; it is the extension point for populating OPC-10030 common-model instances.
See [`samples/ISA95/MinimalIsa95Server/Program.cs`](../samples/ISA95/MinimalIsa95Server/Program.cs) for a complete example that also creates Personnel, PhysicalAsset, Material (class/definition/lot/sublot), and a provider-backed `GeoSpatialLocationType`.
## Quick start — client
```csharp
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua.ISA95.Client;
IIsa95ClientFactory isa95ClientFactory = serviceProvider
.GetRequiredService<IIsa95ClientFactory>();
Isa95Client client = await isa95ClientFactory.ConnectAsync(ct);
Isa95JobControlDiscovery discovery =
await client.DiscoverJobControlAsync(ct).ConfigureAwait(false);
Isa95JobControlEndpoint v2 = discovery.V2Endpoints[0];
Isa95JobControlV2Client jobControl = client.CreateJobControlV2Client(
v2.NodeId,
discovery.V2Endpoints[1].NodeId,
discovery.V2Endpoints[2].NodeId);
ulong status = await jobControl.StoreAndStartAsync(
new Opc.Ua.ISA95.JobControl.V2.ISA95JobOrderDataType
{
JobOrderID = "Job-1",
Priority = 1
},
ct: ct).ConfigureAwait(false);
```
`IIsa95ClientFactory` is registered by `AddIsa95Client`; resolve it through constructor injection, call `ConnectAsync` to lazily acquire the registered `ManagedSession`, or call `Create` with an existing connected `ISession`.
## Server hosting model
`services.AddOpcUa().AddServer(...).AddIsa95Server(...).ConfigureModel(...)` is the full registration surface:
- `AddIsa95Server(configure, configureJobControl)` registers `Isa95ServerOptions`, the default `InMemoryIsa95JobControlProvider` (unless overridden — see below), and adds `Isa95NodeManagerFactory` to the server's node-manager pipeline. It returns `IIsa95ServerBuilder`.
- `IIsa95ServerBuilder.ConfigureModel(Func<IIsa95ModelBuilder, CancellationToken, ValueTask>)` registers a callback invoked once, after the ISA-95 root folder and Job Control endpoints exist, with a typed `IIsa95ModelBuilder` scoped to that node manager.
- `Isa95NodeManager` (`Opc.Ua.ISA95.Server.Isa95NodeManager`) is a single `FluentNodeManagerBase` that owns all three namespaces at once (Common Model, Job Control V1, Job Control V2) — there is one node manager instance, not one per namespace.
- The `GeoSpatialLocationType` variable exposed by the common model is provider-backed by the stack's shared `IGeoLocationProvider` seam, so one provider implementation serves both this model and OPC 10000-211 (GPOS); `Isa95GeoSpatialLocationBinder.Bind` wires an instance's async read hook and background update loop to a provider without ever blocking the stack on synchronous I/O.
**Custom providers.** Register a cohesive custom Job Control provider set **before** calling `AddIsa95Server`. If any Job Control facet is already registered, `AddIsa95Server` does not backfill the other facets from the in-memory provider, preventing unrelated stores from being combined; adding a partial custom provider after the default registration is rejected when the provider aggregate resolves. A complete distributed provider commonly implements `IIsa95JobOrderReceiverV1/V2`, `IIsa95JobResponseProviderV1/V2`, `IIsa95JobResponseReceiverV1/V2`, `IIsa95JobStatusSourceV2`, `IIsa95JobExecutionController`, `IIsa95JobOrderCatalog`, and `IIsa95JobOrderCatalogChangeSource`. The geospatial location seam is independent of Job Control: supply any `IGeoLocationProvider` to `CreateGeoSpatialLocationAsync` or `BindGeoSpatialLocation`.
## Common-model builder reference
`IIsa95ModelBuilder` (implemented by `Isa95ModelBuilder`) exposes typed `Create*Async` factory methods for every OPC-10030 primary object and class type, concrete property factories, and relationship helpers constrained to the normative source and target node classes:
- Creation: `CreatePersonnelClassAsync`, `CreatePersonAsync`, `CreateEquipmentClassAsync`, `CreateEquipmentAsync`, `CreatePhysicalAssetClassAsync`, `CreatePhysicalAssetAsync`, `CreateMaterialClassAsync`, `CreateMaterialDefinitionAsync`, `CreateMaterialLotAsync`, `CreateMaterialSublotAsync`, `CreateEquipmentCapabilityTestSpecificationAsync`, `CreatePhysicalAssetCapabilityTestSpecificationAsync`, `CreateQualificationTestSpecificationAsync`, `CreateMaterialTestSpecificationAsync`, `CreateTestResultAsync`, `AddClassPropertyAsync`, `AddPropertyAsync`.
- Classification/aggregation references: `DefinedByPersonnelClass`, `DefinedByEquipmentClass`, `DefinedByPhysicalAssetClass`, `DefinedByMaterialClass`, `DefinedByMaterialDefinition`, `MadeUpOfEquipment`, `MadeUpOfPhysicalAsset`, `MadeUpOfMaterialSublot`, `AssembledFromClass`, `AssembledFromDefinition`, `AssembledFromLot`, `AssembledFromSublot`.
- Testing references: `TestedByEquipmentTest`, `TestedByPhysicalAssetTest`, `TestedByQualificationTest`, `TestedByMaterialTest`, `HasTestResult`, `ResultsForSpecification`.
- Other: `LocatedIn` (a Variable/VariableType instance to `GeoSpatialLocationType`), bidirectional Equipment/PhysicalAsset `ImplementedBy` overloads, `Relate` (escape hatch for any `NodeId` reference type), `BindGeoSpatialLocation` (wraps `Isa95GeoSpatialLocationBinder.Bind`), `RegisterAsync`/`RemoveAsync` (address-space add/remove through the owning node manager).
`AddClassPropertyAsync` and the owner-specific `AddPropertyAsync` overloads always instantiate the concrete OPC-10030 property type for the supplied owner; the abstract `ISA95ClassPropertyType` and `ISA95PropertyType` are never instantiated. Material sublots use the standard `MaterialLotPropertyType`, as defined by the published model.
`model.Root` is the ISA-95 root `FolderState` created by the node manager (organized under the standard `ObjectsFolder`); every `Create*Async` call defaults to parenting under a supplied parent node, typically `model.Root` or a previously created instance.
## Job Control (V1 and V2)
The default `InMemoryIsa95JobControlProvider` implements every V1 and V2 provider interface over a **single shared, version-neutral store** keyed by job-order ID (`Isa95JobCanonicalState`), so a job order created through one Job Control version is visible and controllable through the other.
| Canonical state | V2 term | V1 term |
|---|---|---|
| `NotAllowedToStart` | NotAllowedToStart | Waiting |
| `AllowedToStart` | AllowedToStart | Ready |
| `Running` | Running | Running |
| `Held` | Interrupted / Held | Held |
| `Suspended` | Interrupted / Suspended | Suspended |
| `Completed` (terminal) | Ended / Completed | Completed |
| `Aborted` (terminal) | Aborted | Aborted |
| `Closed` (terminal) | Ended / Closed | Closed |
| `Loaded` (V1 response state) | Running projection | Loaded |
| `Error` (V1 response state) | Aborted projection | Error |
The neutral store preserves every standard V1/V2 job-order and job-response field, including work masters, personnel/equipment/physical-asset/material requirements and actuals, nested parameters/properties, engineering units, and V2 optional-field `EncodingMask` bits. Same-version round trips are lossless; cross-version views project only the fields representable by the target specification.
V1 behavior remains distinct where the specifications differ: `RequestJobResponse` requires exactly one selector (job-order ID or state), V1 `Stop` removes the stored order, and V1-only `Loaded`/`Error` response states round-trip without being collapsed.
V2 interrupted and ended sub-states use the standard two-entry state path: a top-level `Interrupted`/`Ended` entry with an empty browse path, followed by state number `1` or `2` under the `HasSubStateMachine` path. No private composite state numbers are emitted.
**V2 job-order operations** (`Isa95JobOrderOperationV2`, applied through `IIsa95JobOrderReceiverV2.ReceiveJobOrderAsync`):
| Operation | Effect |
|---|---|
| `Store` | Creates a new order → `NotAllowedToStart`. |
| `StoreAndStart` | Creates a new order → `AllowedToStart`. |
| `Start` | `NotAllowedToStart` → `AllowedToStart`. |
| `Update` | Updates a stored, not-yet-started order in place. |
| `Stop` | `Running`/`Held`/`Suspended` → `Completed` (ends it). |
| `Pause` | `Running` → `Suspended` (interrupts it). |
| `Resume` | `Held`/`Suspended` → `Running`. |
| `Abort` | Any non-terminal state → `Aborted`. |
| `RevokeStart` | `AllowedToStart` → `NotAllowedToStart`. |
| `Cancel` | Removes a `NotAllowedToStart`/`AllowedToStart` order from the store. |
| `Clear` | Removes a terminal (`Completed`/`Aborted`/`Closed`) order from the store. |
**Execution-system transitions** (`Isa95JobExecutionTransition`, applied through `IIsa95JobExecutionController.TransitionAsync`) model the automatic, non-client-driven state changes of the OPC-10031-4 V2 execution-system state machine: `BeginExecution` (`AllowedToStart` → `Running`), `Hold` (`Running` → `Held`), `Complete` (`Running`/`Held`/`Suspended` → `Completed`), `Close` (`Completed` → `Closed`). A hosted service (or any application component) drives these once the execution system — not the OPC UA client — decides to start, hold, complete, or close a job order; `samples/ISA95/MinimalIsa95Server/DemoJobSeeder.cs` calls `StoreAndStart` followed by `BeginExecution` to demonstrate the automatic `AllowedToStart` → `Running` transition.
**Status events.** `IIsa95JobStatusSourceV2.SubscribeAsync` publishes one `Isa95JobStatusNotificationV2` per committed state change to every independent subscriber. Because the standard `ISA95JobOrderStatusEventType` is abstract, `Isa95NodeManager` defines a concrete server-owned subtype and reports that subtype while preserving the standard event fields and type hierarchy. Each notification rewrites `JobResponse.JobState` to the current event state, and the latest non-empty V2 `Comment` array is retained as audit context.
**Catalog changes.** A successful V2 `Update` is a standard self-transition and emits the required status event with the modified order; it also emits an `Updated` catalog change so `JobOrderList` projections refresh. Cancel/Clear/V1-Stop removals emit a `Removed` catalog change without fabricating a life-cycle transition. The node manager consumes `IIsa95JobOrderCatalogChangeSource` so V1/V2 lists stay current even when a shared provider is changed outside the local method handler.
**Return status.** Every Job Control V1/V2 method returns a `UInt64 ReturnStatus` bitmap as defined by Annex B.2 of the two companion specifications; `Isa95JobReturnStatus` names the bits used by this implementation (`Success` = bit 0, `UnknownJobOrderId` = bit 1, `InvalidCommand` = bit 2 (V1), `InvalidStatus` = bit 3, `UnableToAccept` = bit 4, `InvalidRequest` = bit 32) and more than one bit can be set at once.
**Business failures are `Uncertain`, not `Bad`.** When an operation cannot be applied for a business reason (unknown job order, invalid state transition, and similar), the in-memory provider returns `ServiceResult(StatusCodes.Uncertain, ...)` together with the relevant `ReturnStatus` bits, rather than a `Bad_*` status code; the OPC UA `ReturnStatus` output argument is the authoritative outcome channel, and `Uncertain` signals "the call was serviced but the requested business operation did not succeed" as opposed to a protocol-level failure.
**Bounding.** `Isa95JobControlProviderOptions` bounds the in-memory engine: `MaxJobOrders` (default 1024) and `MaxJobResponses` (default 1024) cap concurrent storage and reject new entries beyond the bound; `ResponseRetention` (default `TimeSpan.Zero`, meaning disabled) purges responses older than the configured age using the injected `TimeProvider` before each new response is received or a query is served.
## GeoSpatialLocationType provider seam
OPC-10030's `GeoSpatialLocationType` models a location as **text**, not as coordinates: the type is `DataType=String` with `ValueRank=0 (OneOrMoreDimensions)`, so its value is an array of literals such as Well-Known-Text geometries or postal addresses.
The variable is backed by the stack's shared [`IGeoLocationProvider`](Positioning.md#server-authoring) seam, which lives in `Opc.Ua.Server` (namespace `Opc.Ua`) so it is available to every companion model that publishes location. `Isa95GeoSpatialLocationBinder.Bind` wires an instance's async read hook and background update loop to a provider without ever blocking the stack on synchronous I/O; the loop is only started when the provider reports `SupportsPush`.
**One provider serves both models.** Because the seam is shared, a single provider implementation drives ISA-95 `GeoSpatialLocationType` variables *and* OPC 10000-211 (GPOS) `GlobalLocation` variables. The projection is directional: a structured `GeoPosition` is rendered into text by an `IGeoLocationTextFormatter` (defaulting to `WktGeoLocationTextFormatter`, which emits `POINT Z (lon lat height)` and prefixes `SRID=<epsg>;` when the sample declares one), while a source that only knows a textual location publishes `Labels` and no coordinates. Such a text-only sample remains first class here, and is simply rejected by GPOS, which requires coordinates.
```csharp
using var provider = new InMemoryGeoLocationProvider();
provider.Update("plant", new GeoPosition(47.3769, 8.5417, EpsgCode: 4326));
Isa95GeoSpatialLocationBinding location =
await model.CreateGeoSpatialLocationAsync(
model.Root,
"PlantLocation",
provider,
"plant",
cancellationToken: ct);
// The Variable publishes ["SRID=4326;POINT (8.5417 47.3769)"].
```
Pass a custom `IGeoLocationTextFormatter` to `CreateGeoSpatialLocationAsync` or `BindGeoSpatialLocation` to publish a different geometry encoding or a site-specific literal.
## Client
`Isa95Client` (constructed directly from an `ISession`/`ManagedSession` plus `ITelemetryContext`, or resolved through `AddIsa95Client`) is the discovery entry point:
- `DiscoverCommonObjectsAsync(rootNodeId, recursive, ct)` walks the address space below a root using `Session.ManagedBrowseAsync`, which transparently follows `BrowseNext` continuation points, and returns every OPC-10030 primary object or vendor-defined subtype it finds (`Isa95CommonObjectKind`: PersonnelClass, Person, EquipmentClass, Equipment, PhysicalAssetClass, PhysicalAsset, MaterialClass, MaterialDefinition, MaterialLot, MaterialSublot).
- `DiscoverJobControlAsync(rootNodeId, ct)` (and the `ObjectsFolder`-rooted overload) recognizes the standard V1/V2 endpoint ObjectTypes and vendor-defined subtypes, returning every match in `Isa95JobControlDiscovery.V1Endpoints`/`V2Endpoints` so callers can explicitly handle a missing or ambiguous facet instead of an implicit "first match wins" choice.
- `CreatePersonClient`/`CreateEquipmentClient`/`CreatePhysicalAssetClient`/`CreateMaterialLotClient` return generated `*TypeClient` proxies for direct property access.
- `CreateJobControlV1Client`/`CreateJobControlV2Client` return the direct Job Control clients described below.
`Isa95JobControlV1Client` and `Isa95JobControlV2Client` wrap the three generated endpoint-object proxies (Job Order Receiver, Job Response Provider, Job Response Receiver) behind flat, per-operation async methods (`StoreAsync`, `StoreAndStartAsync`, `StartAsync`, `UpdateAsync`, `StopAsync`, `PauseAsync`, `ResumeAsync`, `AbortAsync`, `RevokeStartAsync`, `CancelAsync`, `ClearAsync` for V2; `ReceiveJobOrderAsync` with an explicit command for V1) plus response query/receive methods. Their public constructors idempotently register all Common/V1/V2 encodeables with the supplied session, so direct standalone use is wire-safe; each still exposes the underlying generated proxy for advanced scenarios.
**Typed V2 status-event streaming.** `Isa95JobControlV2Client.SubscribeJobOrderStatusEventsAsync(streaming, notifierId, registry, options, ct)` builds the generated `ISA95JobOrderStatusEventTypeRecord` event filter, registers the V2 event-record decoders, and decodes each `EventNotification` from an `IStreamingSubscription` (typically `session.DefaultStreaming`) into a strongly-typed record — no manual `EventFieldList` unpacking required:
```csharp
IStreamingSubscription streaming = session.DefaultStreaming;
await foreach (ISA95JobOrderStatusEventTypeRecord record in
jobControl.SubscribeJobOrderStatusEventsAsync(streaming, jobControl.JobResponseProviderId, ct: ct))
{
// record.JobOrderID, record.State, ...
}
```
`AddIsa95Client` (on `IOpcUaBuilder` or `IOpcUaClientBuilder`) registers `ITelemetryContext` and the injectable `IIsa95ClientFactory`. `Create` wraps an existing session; when `Isa95ClientOptions.LazyConnect` is true (the default), `ConnectAsync` lazily acquires the `ManagedSession` registered by `AddClient` on first use and caches the resulting client.
## In-memory limitations and HA guidance
`InMemoryIsa95JobControlProvider` is a single-process, non-durable engine: all job orders, responses, and subscriptions live in process memory guarded by a `System.Threading.Lock`, and are lost on process restart or crash. It is appropriate for development, demos, and single-instance deployments, not for production high-availability topologies.
For a highly-available or horizontally distributed server, implement the provider interfaces (`IIsa95JobOrderReceiverV1/V2`, `IIsa95JobResponseProviderV1/V2`, `IIsa95JobResponseReceiverV1/V2`, `IIsa95JobStatusSourceV2`, `IIsa95JobExecutionController`, `IIsa95JobOrderCatalog`, `IIsa95JobOrderCatalogChangeSource`) against one shared, durable backing store (for example a database or distributed cache reachable by every server instance) and register that cohesive set before calling `AddIsa95Server`. This mirrors the provider-model guidance in [Historical Access](HistoricalAccess.md) and the general redundancy guidance in [High Availability and OPC UA Redundancy](HighAvailability.md): state that must survive failover belongs behind a shared provider, not in server-local memory.
`IIsa95JobStatusSourceV2.SubscribeAsync` and `IIsa95JobOrderCatalogChangeSource.SubscribeCatalogChangesAsync` each provide independent, post-subscription delivery streams. A durable/distributed implementation must preserve the exactly-once-per-committed-mutation contract (or document stronger replay/checkpoint semantics) so event and `JobOrderList` projections remain coherent across server instances.
## Conformance matrix
Status key: ✅ Implemented and tested · 📄 Static NodeSet structure only (no runtime provider/server/client behavior exercises it yet) · 🔲 Optional per the published modelling rule.
This matrix distinguishes **static NodeSet structure** (what the NodeSet2 XML declares, validated by asset-level tests against the embedded XML/CSV) from **runtime-tested facets** (behavior exercised through the server, provider, or client at runtime).
| Area | Static structure | Runtime | Source | Tests |
|---|---|---|---|---|
| Common Model primary types and concrete property types (Person/Equipment/PhysicalAsset/Material class+instance hierarchy) | ✅ | ✅ | [`Isa95ModelBuilder`](../src/Opc.Ua.ISA95.Server/Builders/Isa95ModelBuilder.cs) | `ModelAssetTests`; `Isa95ModelBuilderTests`; `Isa95EndToEndTests` |
| `DefinedByMaterialClass` / `AssembledFromSublot` repairs | ✅ | ✅ | [`Opc.ISA95.NodeSet2.xml`](../src/Opc.Ua.ISA95/Design/Common/Opc.ISA95.NodeSet2.xml) | `ModelAssetTests.CommonModelContainsNormativeMaterialReferenceRepairs` |
| `GeoSpatialLocationType` (String, ValueRank OneOrMoreDimensions), backed by the shared `IGeoLocationProvider` | ✅ | ✅ | [`Isa95GeoSpatialLocationBinder`](../src/Opc.Ua.ISA95.Server/Builders/Isa95GeoSpatialLocationBinder.cs) | `Isa95GeoSpatialLocationTests`; `Isa95EndToEndTests` |
| GPOS/RSL (Part 210/211) coordinate model, shared with ISA-95 through `IGeoLocationProvider` | ✅ | ✅ | [`IGeoLocationProvider`](../src/Opc.Ua.Server/Location/IGeoLocationProvider.cs); [`Positioning`](Positioning.md) | `SharedGeoLocationProviderTests`; `InMemoryGeoLocationProviderTests` |
| Job Control V1 `ReceiveJobOrder`/`RequestJobResponse`/`ReceiveJobResponse` (all mandatory, modelling rule `i=78`) | ✅ | ✅ | [`InMemoryIsa95JobControlProvider`](../src/Opc.Ua.ISA95.Server/Providers/InMemoryIsa95JobControlProvider.cs) | `ModelAssetTests.JobControlV1MethodsAreMandatory`; `InMemoryIsa95JobControlProviderV1Tests` |
| Job Control V2 Job Order Receiver operations (Store/StoreAndStart/Start/Update/Stop/Pause/Resume/Abort/RevokeStart/Cancel/Clear — 🔲 optional, modelling rule `i=80`) | ✅ | ✅ | same | `ModelAssetTests.JobControlV2MethodRulesMatchPublishedModel`; `InMemoryIsa95JobControlProviderV2Tests` |
| Job Control V2 response methods (`RequestJobResponseByJobOrderID`, `RequestJobResponseByJobOrderState`, `ReceiveJobResponse` — mandatory, modelling rule `i=78`) | ✅ | ✅ | same | same |
| Job Control V2 state-machine causes and standard state paths (`HasCause`, `HasSubStateMachine`, scoped sub-state numbers) | ✅ | ✅ | same | `ModelAssetTests.JobControlV2StateMachineHasPublishedCauses`; `Isa95V2StateMachineTests` |
| Job Control V1/V2 lossless fields, V1 selectors/Stop/Loaded/Error, and V2 comments | ✅ | ✅ | [`Isa95JobControlConversions`](../src/Opc.Ua.ISA95.Server/Providers/Internal/Isa95JobControlConversions.cs) | `Isa95JobControlConversionsTests`; `InMemoryIsa95JobControlProviderV1Tests`; `InMemoryIsa95JobControlProviderV2Tests` |
| Job Control V2 abstract status-event shape, concrete server subtype, and live delivery | ✅ | ✅ | [`Isa95NodeManager`](../src/Opc.Ua.ISA95.Server/Isa95NodeManager.cs) | `ModelAssetTests.JobControlV2StatusEventHasRequiredShape`; `Isa95JobControlClientTests`; `Isa95EndToEndTests` |
| Catalog mutation projection (`Updated`/`Removed`) | — | ✅ | [`IIsa95JobOrderCatalogChangeSource`](../src/Opc.Ua.ISA95.Server/Providers/IIsa95JobOrderCatalogChangeSource.cs) | `InMemoryIsa95JobControlProviderV2Tests`; `Isa95EndToEndTests` |
| `ReturnStatus` Annex B bitmap / `Uncertain` business-failure result | — | ✅ | [`Isa95JobReturnStatus`](../src/Opc.Ua.ISA95.Server/Providers/Isa95JobReturnStatus.cs) | `InMemoryIsa95JobControlProviderV1Tests`; `InMemoryIsa95JobControlProviderV2Tests` |
| Subtype-aware client discovery (common objects + Job Control endpoints), continuation-safe via `ManagedBrowseAsync` | — | ✅ | [`Isa95Client`](../src/Opc.Ua.ISA95.Client/Isa95Client.cs) | `Isa95JobControlClientTests`; `Isa95EndToEndTests` |
| Typed V2 status-event client streaming | — | ✅ | [`Isa95JobControlV2Client`](../src/Opc.Ua.ISA95.Client/Isa95JobControlV2Client.cs) | `Isa95JobControlClientTests`; `Isa95EndToEndTests` |
| Cohesive server/client DI wiring (`AddIsa95Server`, `AddIsa95Client`) | — | ✅ | [`OpcUaIsa95ServerBuilderExtensions`](../src/Opc.Ua.ISA95.Server/Hosting/OpcUaIsa95ServerBuilderExtensions.cs), [`OpcUaIsa95ClientBuilderExtensions`](../src/Opc.Ua.ISA95.Client/Hosting/OpcUaIsa95ClientBuilderExtensions.cs) | `Isa95ServerBuilderTests`; `Isa95JobControlProviderOptionsTests` |
| Persistent NativeAOT roots | — | ✅ | [`Isa95AotTests`](../tests/Opc.Ua.Aot.Tests/Isa95AotTests.cs) | NativeAOT publish + source-generated test execution |
| Durable/HA Job Control provider | ❌ not shipped (in-memory only; see [In-memory limitations and HA guidance](#in-memory-limitations-and-ha-guidance)) | — | — | — |
## Model sources, identifier mappings, and normative repairs
All three NodeSet2 XML documents live under [`src/Opc.Ua.ISA95/Design`](../src/Opc.Ua.ISA95/Design) and are compiled by the repository's model source generator (`Opc.Ua.SourceGeneration`, `ModelSourceGeneratorVersion=v105`).
Each NodeSet2 XML file has a hand-maintained identifier mapping CSV (`*.NodeIds.csv`) that pins every symbolic name to its numeric NodeId and NodeClass. The V1 and V2 folders also contain an unmodified `*.Upstream.NodeIds.csv` copy of the identifiers published by the OPC Foundation so the generated mapping can be compared directly with its source. The Common Model has no separate upstream mapping because its local mapping includes the two normative repairs described below.
`Tests/Opc.Ua.ISA95.Tests/ModelAssetTests.cs` verifies that every mapping row resolves to the matching NodeId and NodeClass, every numeric NodeSet node is represented, model identity and node counts remain stable, mandatory and optional method rules match the specifications, and the V2 status-event and state-machine references retain their published shape.
### Provenance
The model assets were derived from these upstream source revisions:
| Asset | Upstream source commit |
|---|---|
| Common Model (`Design/Common`) | `2a11da3dee11e623ce552f956ece0a571cc6e571` |
| Job Control V1 (`Design/JobControl/V1`) | `90d4ebe362b8cba3d745c7760fda187c35b4f9f4` |
| Job Control V2 (`Design/JobControl/V2`) | `e0b5d80cfff698f0276393e28df138a7fbc4b541` |
`ModelAssetTests.CanonicalNodeSetHasExpectedIdentity` pins each NodeSet2's `ModelUri`, `Version`, publication date, and total node count (Common: 390, Job Control V1: 91, Job Control V2: 258), preventing accidental upstream drift or unreviewed local edits.
### Normative Common Model repairs
The published OPC-10030 v1.00 NodeSet2 XML has two gaps against the specification text. Both are corrected transparently in [`src/Opc.Ua.ISA95/Design/Common/Opc.ISA95.NodeSet2.xml`](../src/Opc.Ua.ISA95/Design/Common/Opc.ISA95.NodeSet2.xml) with an inline XML comment at each repair site.
**`DefinedByMaterialClass` (assigned `ns=1;i=5300`).** OPC-10030 §9.6.2 and Table 76 require this ReferenceType so that `MaterialDefinitionType` can declare its `<MaterialClass>` reference, but the published NodeSet omits the declaration and leaves that reference orphaned. NodeId 5300 is the unused identifier immediately preceding the published `DefinedByMaterialDefinition` (5301), so it does not collide with any published identifier.
**`AssembledFromSublot` (assigned `ns=1;i=5333`).** OPC-10030 §9.6.8 and Table 77 require this ReferenceType so that `MaterialLotType` can declare its `<AssemblySublot>` reference, but the published NodeSet omits it in the same way. Identifier 5333 follows the published model's highest assigned identifier and is collision-free.
The repairs restore each ReferenceType declaration with the correct `HasSubtype` supertype, `InverseName`, and specification link, and wire the previously orphaned forward/inverse references on `MaterialDefinitionType` (`ns=1;i=5219`) and `MaterialLotType` (`ns=1;i=5232`, `ns=1;i=5259`). `ModelAssetTests.CommonModelContainsNormativeMaterialReferenceRepairs` verifies the repaired source and target NodeIds.