# Description Implement the released [OPC 40100-1](https://reference.opcfoundation.org/specs/OPC-40100-1) and [OPC 40100-2](https://reference.opcfoundation.org/specs/OPC-40100-2) specifications with generated models, provider-backed runtimes, typed clients, MCP workflows, and DI/Machinery integration. - **Part 1 stays in `Opc.Ua.Vision`** and its client/server packages alongside the distinct Perception model. Provide control/acquisition state machines, jobs and synchronized steps, recipe/product/configuration/result management, file transfers, diagnostics, events, and conditions. - **Part 2 is in `Opc.Ua.MachineVision`, `.Client`, and `.Server`.** Provide asset categories, identification, image-handling aspects, health/maintenance data, topology, and transactional updates. The base Vision assemblies do not reference these optional packages; the Part 2 wire model does not depend on Part 1. - Preserve the release-pinned model bytes, wire namespaces and NodeIds. Perception types are in `Opc.Ua.Vision.Perception`; the AMCM generated namespace remains `Opc.Ua.Vision.MachineVision.AMCM`, and asset client/server extension namespaces remain `Opc.Ua.Vision.Client` and `Opc.Ua.Vision.Server`. - Use `AddOpcUaMachineVision` and `AddOpcUaMachineVisionAMCM` for model loading and factory registration. Each has node-collection, encodeable-factory-builder and node-state-factory-builder overloads. These short entry points delegate to the generated implementations; existing namespace-derived generated names remain available for compatibility. - Support multiple independent named systems/assets under one namespace-owning manager. Reuse injectable providers, file binding, bounded memory stores, optional transactional `IFileSystemProvider` persistence, state machines, telemetry and conformance infrastructure. - Keep one `docs/Vision.md` guide, titled `Machine Vision (OPC 40100-1 and OPC 40100-2)`, and one `Opc.Ua.Mcp.Vision` package. Include deterministic server/client samples. - Expose 17 Machine Vision MCP tools, including capabilities/status, bounded relationships and observation, complete small-file uploads/downloads, management, explicit control, conditions, and chunked transfers. Catalogue counts: 39 Vision-specific, 43 with connection tools, 81 Vision+Robotics, 155 Full. Every Machine Vision wire parameter has a schema description. **Compatibility and limits:** `AddVision()` remains Perception-only unless released parts are selected. Standalone Part 2 uses `AddVision().AddMachineVision()`; Machinery composition uses `AddVisionToMachinery()`. Existing tool defaults and command signatures are preserved. Observation owns and cleans up its subscription: 5 seconds/50 notifications by default, capped at 60 seconds/100 notifications. Complete-file tools are capped at 1 MiB; larger files use chunked transfers. Domain Error values remain distinct from UA service failures. **Versioning:** the seven Vision/MachineVision/MCP packages and their Debug variants follow the repository root version instead of being forced to preview. Vision.OpenUsd and its descendants remain preview-only. Development builds still inherit the root prerelease version. No release version, tag, release branch, publication policy, or feed promotion is changed. **Review actions:** all fourteen review threads have action/evidence replies and are resolved. Changes include the unified guide, Perception namespace, separate Part 2 packages and final naming, multi-system ownership explanation, interrupted-write cleanup, observer failure isolation, complete MCP parameter descriptions, and explicit package inventory. The preview-layout migration topic and the requested ReleaseProcess caveat were removed. The test Publish collector now retries recognized transient failures within one cancellation budget and propagates other exceptions. Commit69b1f5db9applies the requested short loader names and renames standalone hosting to `AddMachineVision`, with callers, tests and documentation updated. The intervening master merge is preserved. ### Verification **Latest API naming changes, verified locally:** | Scope | Result | | --- | --- | | Full Vision suite, net10 | 572 passed, 1 existing skip, 0 failed | | Full Vision suite, net48 | 515 passed, 0 skipped, 0 failed | | Final loader/hosting cases after cross-TFM null-guard adjustment | 9 passed, 0 failed, 0 skipped | | Machine Vision MCP and profile cases | 198 passed, 0 failed, 0 skipped | | Main and MCP AOT test-project compilation | Both built with 0 warnings/errors | | MCP AOT companion, managed execution | 5 passed, 0 failed, 0 skipped; not claimed as native execution | `ModelLoaderNamesRegisterTypedFactories`, `ModelLoaderNamesRejectNullArguments`, and `GeneratedModelsLoadTogetherAndCreateTypedInstances` cover all six loader overloads, fluent return identities, typed instances, namespace coexistence and null guards. Existing standalone Part 2 tests verify idempotent registration, one-owner enforcement and null handling through `AddMachineVision`. No source-generator implementation, CLR namespace, wire identity or model payload was changed for these naming updates. The current69b1f5db9revision is running [CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826937), [package validation](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165823359), [MCP usability](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826874), [CodeQL](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826906), and [Images CI](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38165826884). These runs are not claimed as completed results. **Earlier completed verification:** all seven default-TFM package archives were inspected after the package rename: exact IDs, 27 clean DLLs, correct dependencies/readmes/licenses, and unchanged pinned model hashes. [CI onc75ec4fa6](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/38142519760) passed, including the formerly failing Linux batch and required summary. Its unchanged coverage gate passed at **88.56% lines, 78.96% branches, 84.28% patch** against the 75% patch floor. The later bounded-Publish-helper regressions reproduced 13 failures before the fix and passed afterward; the full mainline triggering fixture passed 67 tests on each of net10/net48. These are historical results, not current full-matrix results. <details> <summary>Executed API naming validation commands and outcomes</summary> Run from the worktree root using the established artifact trees. Dependency builds were performed before the no-build full net10 run; the focused loader/hosting selection was rebuilt and rerun after the final null-guard adjustment. ```powershell $files = 'C:\Users\mschier\.copilot\session-state\deb2ba10-b0e9-4a44-8783-8331bc850814\files' $env:DOTNET_CLI_USE_MSBUILD_SERVER = '0' $env:TEMP = 'C:\Temp\mv-deb2\followup' $env:TMP = $env:TEMP # Passed: 572; Failed: 0; Skipped: 1. dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-build --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net10" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup' # Passed: 9; Failed: 0; Skipped: 0, on the final source. dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~MachineVisionModelTests|FullyQualifiedName~StandalonePartTwoRegistrationUsesOneOwnerAndOneModelProvider|FullyQualifiedName~StandalonePartTwoRequiresRegistrationAndRejectsCompetingOwnership' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-model" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup' # Passed: 198; Failed: 0; Skipped: 0. dotnet test tests\Opc.Ua.Tools.Tests\Opc.Ua.Tools.Tests.csproj -c Release -f net10.0 --no-restore --artifacts-path artifacts\vision-followup -p:CustomTestTarget=net10.0 -p:UseSharedCompilation=false -m:1 -nr:false --filter 'FullyQualifiedName~VisionMachineToolsTests|FullyQualifiedName~McpHostBuilderTests|FullyQualifiedName~McpToolProfileCompositionTests' --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-final-mcp" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup' $env:TEMP = 'C:\Temp\mv-deb2\followup48' $env:TMP = $env:TEMP # Passed: 515; Failed: 0; Skipped: 0. dotnet test tests\Opc.Ua.Vision.Tests\Opc.Ua.Vision.Tests.csproj -c Debug -f net48 --no-restore --artifacts-path "$files\solution-net48-artifacts" -p:CustomTestTarget=net48 -p:UseSharedCompilation=false -m:1 -nr:false --logger 'console;verbosity=minimal' --logger trx --results-directory "$files\vision-api-names-net48-final" -- 'NUnit.WorkDirectory=C:\Temp\mv-deb2\followup48' ``` Opt-in long-running/stress/durable tiers have not been rerun locally; the unqualified **all tests** checklist item remains open. Original implementation matrix counts are not presented as current results. </details> ## Related Issues - Fixes #4732. The requester reviewed and approved the implementation plan and subsequent packaging/API decisions in the development session. The tracking issue records the design; maintainer architectural discussion/sign-off there is still pending. This PR remains a draft. ## 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. - [x] 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. - [x] 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. - [x] I have addressed **all** PR feedback received.
21 KiB
FileSystemClient — a System.IO-style async client for OPC UA file systems
FileSystemClient (in src/Opc.Ua.Client/FileSystem/, namespace
Opc.Ua.Client.FileSystem) is an ergonomic, async-only wrapper around the
OPC UA file-system primitives defined in Part 5 §C and Part 20 §4 —
the FileType, FileDirectoryType, and TemporaryFileTransferType
ObjectTypes. It is layered on top of the source-generated proxies emitted
into Opc.Ua.Core (FileTypeClient, FileDirectoryTypeClient,
TemporaryFileTransferTypeClient) and exposes a surface that closely
mirrors System.IO.{File, Directory, FileInfo, DirectoryInfo, FileStream}
to make remote file-system navigation feel like working with a local disk.
Quick reference
| Type | Mirrors | Purpose |
|---|---|---|
FileSystemClient |
System.IO.Directory + System.IO.File |
Top-level entry point. Exposes path-based operations (GetFileAsync, CreateDirectoryAsync, DeleteAsync, MoveAsync, CopyAsync, ReadAllBytesAsync, …). Rooted at any FileDirectoryType instance. |
UaFileSystemInfo |
System.IO.FileSystemInfo |
Abstract base for UaFileInfo and UaDirectoryInfo. Carries the resolved NodeId, parent reference, browse name, and canonical path. |
UaFileInfo |
System.IO.FileInfo |
A single file. Lazy-loaded Size / Writable / MimeType / etc. metadata; OpenAsync / OpenReadAsync / OpenWriteAsync returning a UaFileStream; bulk ReadAllBytes / WriteAllText shortcuts. |
UaDirectoryInfo |
System.IO.DirectoryInfo |
A single directory. EnumerateAsync / EnumerateFilesAsync / EnumerateDirectoriesAsync (IAsyncEnumerable<>); CreateSubdirectoryAsync, CreateFileAsync, DeleteAsync(recursive). |
UaFileStream |
System.IO.FileStream |
A Stream-derived wrapper around an open server file handle. Async members hit the wire via FileTypeClient; sync members forward via GetAwaiter().GetResult(). |
UaPath |
System.IO.Path |
Helpers for parsing, formatting, combining, and splitting OPC UA file paths. |
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
- Getting started
- Path syntax
- Path resolution and caching
- Type classification
- File metadata
- Streams
- Error mapping
- Move, copy, and delete semantics
- Temporary file transfer
- What lives where
- Server-side file bindings
- See also
Getting started
Open the standard server file system
Many OPC UA servers expose the standard Server.FileSystem object
(NodeId i=16314):
using Opc.Ua.Client.FileSystem;
ISession session = /* an active OPC UA client session */;
var fs = FileSystemClient.OpenServerFileSystem(session);
await foreach (UaFileSystemInfo entry in fs.EnumerateAsync("/"))
{
Console.WriteLine($"{(entry.IsDirectory ? "DIR " : "FILE")} {entry.FullPath}");
}
Open any FileDirectoryType instance
NodeId vendorRoot = /* result of a Browse / TranslateBrowsePathsToNodeIds */;
var fs = new FileSystemClient(session, vendorRoot);
Read a file
string content = await fs.ReadAllTextAsync("/Reports/2024/summary.json");
Stream a large file
await using UaFileStream stream = await fs.OpenReadAsync("/Logs/server.log");
using var reader = new System.IO.StreamReader(stream);
string firstLine = await reader.ReadLineAsync();
Create a directory tree and write a file
UaDirectoryInfo dir = await fs.CreateDirectoryAsync("/Uploads/2024-05/test", createIntermediate: true);
await fs.WriteAllBytesAsync($"{dir.FullPath}/payload.bin", payload);
Move and delete
UaFileInfo file = await fs.GetFileAsync("/Drafts/v1.txt");
await file.MoveToAsync("/Published/v1.txt");
await fs.DeleteAsync("/Drafts", recursive: true);
Path syntax
Paths use the forward slash '/' as the segment separator. Each segment
is parsed by QualifiedName.Parse(...), so the standard
"<ns>:<name>" form is supported. Examples:
| Path | Meaning |
|---|---|
"" or "/" |
The root directory |
"foo" |
The child foo (namespace 0) of the root |
"/foo/bar" |
Same as foo/bar; leading slash is optional |
"1:Reports/1:2024/data.csv" |
Qualified path using namespace index 1 for the first two segments and namespace 0 for data.csv |
UaPath.Combine, UaPath.GetDirectoryName, UaPath.GetFileName, and
UaPath.Normalize mirror the equivalents on System.IO.Path. Canonical
paths returned from UaFileSystemInfo.FullPath always include the
namespace prefix when it is non-zero, so siblings with the same Name
in different namespaces are never collapsed.
Path resolution and caching
Path → NodeId resolution uses
TranslateBrowsePathsToNodeIdsAsync segment-by-segment, with each step
following ReferenceTypeIds.HierarchicalReferences (with subtypes).
(parent NodeId, browse name) → child NodeId mappings are cached in a
small LRU controlled by FileSystemClientOptions.PathCacheSize (default
1024; set to zero to disable). The cache is best-effort: when a
resolved entry stops working (BadNodeIdUnknown from the server), the
entry is evicted and the path is re-resolved before the error propagates.
After any successful create/delete/move/copy, the path-cache entries
rooted at the affected parent are invalidated to avoid serving stale
NodeIds.
Type classification (FileType vs FileDirectoryType)
Each child returned by EnumerateAsync is classified by its
HasTypeDefinition reference. Subtype-aware classification uses
Session.TypeTree.IsTypeOf(...) — by default subtypes count, so
TrustListType, AddressSpaceFileType, ConfigurationFileType etc.
appear as files. To restrict enumeration to the exact FileType /
FileDirectoryType set:
var fs = new FileSystemClient(session, ObjectIds.FileSystem,
new FileSystemClientOptions
{
IncludeFileTypeSubtypes = false,
IncludeFileDirectoryTypeSubtypes = false,
});
File metadata
UaFileInfo exposes the seven well-known FileType properties (Size,
Writable, UserWritable, OpenCount, MimeType,
MaxByteStringLength, LastModifiedTime). They are populated lazily by
UaFileInfo.RefreshAsync (a single batched Read against the
properties resolved via TranslateBrowsePathsToNodeIds). The optional
properties (MimeType, MaxByteStringLength, LastModifiedTime) are
returned as null when the server does not expose them
(BadNoMatch, BadNodeIdUnknown, or empty target lists are tolerated).
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
authoritative answer.
Streams
UaFileStream derives from System.IO.Stream and supports both async
(ReadAsync, WriteAsync, DisposeAsync) and sync (Read, Write,
Dispose) members. The sync members forward to the async ones via
GetAwaiter().GetResult() — this means they can deadlock on
single-threaded synchronization contexts (e.g. WPF UI thread). Prefer
the async overrides.
Reads and writes are chunked at FileSystemClientOptions.ChunkSize,
clamped down to FileType.MaxByteStringLength when the server advertises
a smaller maximum. Empty ByteString returns are interpreted as EOF;
zero-length reads/writes never hit the wire.
Path-based WriteAllBytesAsync and WriteAllTextAsync refresh metadata
for existing and newly created files before opening them, so these helpers
honor the same per-file chunk limit as OpenWriteAsync.
Position is tracked locally; the server is informed via
SetPosition only when the local cursor diverges from the last
successfully transmitted position.
After a failed read or write, the server cursor is treated as unknown:
the next operation sends SetPosition to the last locally confirmed
position before transferring more bytes. This prevents a lost response
from causing a retry to skip or duplicate a chunk. Successfully completed
earlier chunks remain reflected in Position.
Length is tracked locally too —
opened from FileType.Size at construction and bumped whenever a write
extends past it. Callers that mutate the underlying file through other
handles (or other clients) should call UaFileInfo.RefreshAsync()
before relying on Length.
UaFileStream is not thread-safe in the sense that multiple threads can
share a single instance: concurrent calls are serialised by an internal
SemaphoreSlim, matching FileStream's "not thread-safe but doesn't
corrupt" contract. DisposeAsync issues FileType.Close exactly once
even when called concurrently.
Server-issued file handles are opaque and bound to the Session that opened them. They cannot be used by another Session, and the server automatically closes all remaining handles when their owning Session closes.
Error mapping
OPC UA Bad status codes returned by the server are translated into the
familiar System.IO exception family at the public boundary:
| StatusCode | Mapped exception |
|---|---|
BadNoMatch, BadNodeIdUnknown, BadNotFound |
FileNotFoundException (files) / DirectoryNotFoundException (directories) |
BadBrowseNameDuplicated |
IOException("already exists: …") |
BadUserAccessDenied, BadNotWritable, BadWriteNotSupported, BadSecurityChecksFailed |
UnauthorizedAccessException |
BadInvalidArgument, BadOutOfRange, BadInvalidState, BadResourceUnavailable, BadOutOfMemory |
IOException |
Other Bad… codes |
ServiceResultException (preserved unchanged) |
Mapped exceptions wrap the original ServiceResultException as
InnerException, so callers that need the raw OPC UA status code can
still retrieve it.
TranslateBrowsePathsToNodeIds returning more than one target throws
IOException("ambiguous path: …"); the client never silently picks the
first match.
Move, copy, and delete semantics
These three operations are routed through the source's parent
directory (per Part 20 §4.3) — the server's MoveOrCopy and Delete
methods take a NodeId and operate from the directory's perspective.
DeleteAsync(recursive: false) on a directory first enumerates the
directory and throws IOException("directory not empty: …") if any
child exists. DeleteAsync(recursive: true) invokes the server's
Delete exactly once and lets the server perform recursive removal
(per spec). The client never walks the tree itself for a recursive
delete — that would weaken the server's atomicity / locking guarantees
and double-traverse.
CreateDirectoryAsync and CreateFileAsync accept only a plain
string for the leaf name; they reject leaf segments with a namespace
prefix (the server picks the BrowseName namespace). After the server
returns the new NodeId, the client reads its actual BrowseName and
uses that for the canonical path / cache entry.
CreateFileAsync always passes requestFileOpen: false to the server
— we never leak a server-allocated handle out of the create call.
Callers that want an immediate stream should call UaFileInfo.OpenAsync
afterwards.
Temporary file transfer (Part 5 §C.5)
The temporary-file-transfer pattern is exposed on a separate surface
because its lifecycle does not fit the System.IO abstraction. The
server allocates a transient file, the client streams data through it,
and a final commit (or rollback) tells the server to either publish or
discard the result.
using Opc.Ua.Client.FileSystem;
NodeId transferObject = /* TemporaryFileTransferType instance */;
var temp = new TemporaryFileTransferClient(session, transferObject);
// Read flow
await using UaFileStream readStream = await temp
.GenerateFileForReadAsync(generateOptions: default);
byte[] payload = ReadAll(readStream);
// Write flow
await using UaTemporaryWriteFile write = await temp.GenerateFileForWriteAsync();
await write.Stream.WriteAsync(payload, 0, payload.Length);
NodeId completion = await write.CommitAsync(); // CloseAndCommit
UaTemporaryWriteFile owns the close lifecycle: exactly one terminal
call — CommitAsync (CloseAndCommit) or DisposeAsync (Close,
implicit server rollback) — is sent to the server. The wrapped
Stream cannot accidentally close the server handle; its Dispose
is a no-op.
What lives where
src/Opc.Ua.Client/FileSystem/
├── FileSystemClient.cs # entry point
├── FileSystemClientOptions.cs # tuning knobs
├── FileMetadata.cs # internal property snapshot
├── FileSystemErrors.cs # status code → IO exception mapper
├── PathCache.cs # internal LRU
├── UaPath.cs # System.IO.Path mirror
├── UaFileMode.cs # OpenFileMode flags wrapper
├── UaFileSystemInfo.cs # abstract base
├── UaFileInfo.cs # System.IO.FileInfo mirror
├── UaDirectoryInfo.cs # System.IO.DirectoryInfo mirror
├── UaFileStream.cs # System.IO.Stream mirror
├── TemporaryFileTransferClient.cs # Part 5 §C.5 surface
└── UaTemporaryWriteFile.cs # commit/rollback wrapper
Tests live under tests/Opc.Ua.Client.Tests/FileSystem/.
Server-side file bindings
IFileTypeBinder / FileTypeBinder binds an already-created FileState to
bounded in-memory content and an optional asynchronous commit callback, without
a separate file-system node manager. It shares FileHandle ownership/mode rules
and the existing response-size calculation. Bindings allow at most 64 open
handles, one writer, and independent reader snapshots. Temporary writers commit
through CommitAsync; commitOnClose selects the ordinary FileType close lifecycle.
Application commit callbacks run without holding the binding's operation gate, so
publication can refresh other file bindings. Disposal cancels and drains an active
commit before releasing its handles. Machine Vision uses this
surface for its specialized transfers and recipe files.
Both lazy FileSystemNodeManager nodes and materialized
FileDirectoryBinder nodes protect their mounted root from deletion,
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 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
the protected empty root. Nonroot paths use forward slashes and contain no
empty, . or .. segments. On Windows, backslashes and drive/stream
qualifiers are rejected; on Unix, colon and backslash are ordinary name
characters and remain addressable. Valid relative paths are passed unchanged.
The root remains a valid move/copy destination for a child, but never
the object being deleted, moved, or copied.
FileType.Read requires a positive length and may return fewer bytes than
requested. The server advertises MaxByteStringLength and bounds each
read by the file's limit, the server byte-string limit, and the available
single-result binary response budget. If server limits are disabled,
the default encoding byte-string limit still bounds individual read
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. 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
capability and does not become false merely because a handle is open.
Providers with multiple spellings for one file can implement
IFileSystemPathIdentityProvider. The physical provider uses Windows
case-insensitive identity and Win32 trailing-dot/space normalization, so
alternate NodeIds share OpenCount and writer exclusion. Metadata reads
query the provider asynchronously without allocating retained handle state.
Closed or failed opens release idle handle bags; pending opens remain
protected by their reservations. Extended Windows roots (\\?\...) retain
literal trailing-dot names rather than conflating distinct directory entries.
Materialized directory bindings serialize capacity admission, provider
mutations, refresh, and teardown. Creates and cross-directory moves/copies
are rejected before touching the provider when the target directory is
already at MaxEntries; a rename within the same directory needs no extra
slot. Lookup snapshots are published only after node registration completes.
Teardown retires lookups and handles before provider cleanup and drains
queued operations before disposing its synchronization resources.
IFileSystemProvider.CreateDirectoryAsync remains idempotent, including
package-upload retries. The OPC UA CreateDirectory method checks the
provider for an existing entry and returns BadBrowseNameDuplicated
without creating or replacing it, independently of the provider.
FileDirectoryBinder.BindAsync registers the complete pre-existing tree
before returning, including files on read-only mounts. Supply the
registerNode and deregisterNode callbacks directly on its single
signature. Disposal deregisters materialized nodes from leaves to roots,
closes handles, and detaches file and directory callbacks. The supplied
root remains owned by its original node manager.
If a provider mutation commits but the subsequent refresh fails, the
mutation remains successful and an error identifies the unsynchronized
binding. RefreshAsync retries reconciliation, as does the next mutation
before it changes the provider again. A recoverable pre-mutation refresh
failure is also logged, without blocking a corrective mutation such as
deleting an entry after external changes exceeded MaxEntries. The error
records whether the current provider mutation has committed. Capacity
admission and provider errors still apply; cancellation before commit
still stops the operation. Reconciliation does not truncate over-limit
directories, and an explicit RefreshAsync still reports its failure to
the caller. Failed registration callbacks are retried rather than treating
an unregistered node as complete.
See also
- SourceGeneratedDataTypes.md — for an
overview of the generated
*TypeClientproxies the FileSystem client builds on. - OPC UA Part 5 Annex C (FileType, TemporaryFileTransferType).
- OPC UA Part 20 §4 (FileSystem object model).