diff --git a/docs/AliasNames.md b/docs/AliasNames.md index 36e17b9f12..8d7b21bbda 100644 --- a/docs/AliasNames.md +++ b/docs/AliasNames.md @@ -34,8 +34,9 @@ The server library exposes a pluggable backend (`IAliasNameStore`) plus a default in-memory implementation. Apps assemble their alias inventory inside a store, then either: -1. Register the store directly with the server-wide - `IAliasNameStoreRegistry` so the standard well-known +1. Register the store through `AddAliasNameStore(...)` on the DI server + builder, or directly with the server-wide `IAliasNameStoreRegistry`, + so the standard well-known `Aliases`/`TagVariables`/`Topics` nodes start dispatching through it, **or** 2. Wrap the store in an `AliasNameNodeManager` to expose application- @@ -47,32 +48,55 @@ Both approaches can be combined. ### Quick start — serving standard categories ```csharp +using Microsoft.Extensions.DependencyInjection; using Opc.Ua; -using Opc.Ua.Server; using Opc.Ua.Server.AliasNames; -// inside CreateMasterNodeManager(IServerInternal server, ...): var tagVariables = new AliasNameCategoryDescriptor( ObjectIds.TagVariables, QualifiedName.From(BrowseNames.TagVariables), AliasNameCapabilities.FindAliasVerbose); var store = new InMemoryAliasNameStore([tagVariables]); -store.Seed(ObjectIds.TagVariables, "TIC101_Setpoint", - new ExpandedNodeId("Scalar_Static_Double", refServerNs), +store.Seed(ObjectIds.TagVariables, "ServerCurrentTime", + new ExpandedNodeId(VariableIds.Server_ServerStatus_CurrentTime), serverUri: null, referenceTypeId: ReferenceTypeIds.AliasFor); -// ... seed more entries ... -((IAliasNameStoreRegistryProvider)server) - .AliasNameStoreRegistry.Register(store); +services.AddOpcUa() + .AddServer(options => + { + options.ApplicationName = "AliasServer"; + options.EndpointUrls.Add("opc.tcp://localhost:4840/AliasServer"); + }) + .AddAliasNameStore(store) + .ConfigureAliasNames(options => options.MaterializeAliasNodes = true); ``` +`AddAliasNameStore(...)` and `AddAliasNameStoreRegistry(...)` register +their stores before address-space startup. The opt-in +`ConfigureAliasNames(Action)` extension is on +`IOpcUaServerBuilder` in `Microsoft.Extensions.DependencyInjection`. +`AliasNameServerOptions` is in `Opc.Ua.Server.AliasNames`, and its +`MaterializeAliasNodes` property defaults to `false`. + +When enabled, the normal `ConfigurationNodeManager` materializes +registered standard-category aliases and their declared optional +capabilities after the standard nodes have loaded. No server or +diagnostics-node-manager subclass is needed. The same setting applies +to stores for `Topics`; their targets must be `PublishedDataSetType` +instances. Omit `ConfigureAliasNames(...)` when only method-based +lookup is needed. + When a client calls `Aliases.FindAlias` (`i=23476`), `TagVariables.FindAlias` (`i=23485`) or `Topics.FindAlias` (`i=23494`), `DiagnosticsNodeManager`'s late binder routes the call through the registry to the matching store. +Direct `IAliasNameStoreRegistry` registration remains available for +custom hosting. Register stores before the materialization pass when +their aliases must also be browsable. + ### Quick start — application-defined categories `AliasNameNodeManager` is a `CustomNodeManager2` that owns a namespace @@ -96,7 +120,8 @@ Options: NodeId must lie in this namespace; descriptors pointing anywhere else are skipped with a warning rather than claiming another manager's ids. -* `MaterializeAliasNodes` (default `true`) — creates one browsable +* `MaterializeAliasNodes` (default `true`, unchanged for + `AliasNameNodeManagerOptions`) — creates one browsable `AliasNameType` node per alias, with `AliasFor` references to its targets, exactly as the standard-node materialization below does. Disable to expose only the category tree. @@ -127,6 +152,15 @@ the standard well-known nodes — run the same shared walker (`AliasNameNodeMaterializer`), so they produce structurally identical Part 17 trees and every fix lands in both at once. +For the standard DI server, `ConfigureAliasNames(options => +options.MaterializeAliasNodes = true)` makes the normal +`ConfigurationNodeManager` invoke that helper. Its default is `false`, +independent of the custom `AliasNameNodeManagerOptions` default of +`true`. Metadata, resource, and alias configuration use the +`DependencyInjectionStandardServer` hooks; a non-DI custom +`AddServer()` rejects these DI-only settings rather than +silently ignoring them. + `DiagnosticsNodeManager.MaterializeRegisteredAliasNameNodesAsync` does that for every registered store: one `AliasNameType` instance per alias — BrowseName carrying the alias name, `AliasFor` references to its @@ -143,8 +177,9 @@ namespace; Part 17 clients compare alias names ignoring the namespace, so this is transparent to them. The pass is idempotent, and only creates category nodes whose NodeId lies in the diagnostics namespace — a descriptor pointing anywhere else is skipped with a warning rather -than claiming another manager's (or the standard NodeSet's) ids. Call -it from an overridden `CreateAddressSpaceAsync`, after `base` has +than claiming another manager's (or the standard NodeSet's) ids. +Custom diagnostics-node-manager implementations can still call the +helper from an overridden `CreateAddressSpaceAsync`, after `base` has loaded the standard categories: ```csharp @@ -160,7 +195,7 @@ public override async ValueTask CreateAddressSpaceAsync( } ``` -The same pass also instantiates the **optional Part 17 methods** — +The same pass also instantiates the **optional Part 17 members** — `FindAliasVerbose`, `AddAliasesToCategory`, `DeleteAliasesFromCategory` and `LastChange` — on every category whose descriptor declares them through `AliasNameCapabilities`, including the well-known ones the @@ -176,9 +211,10 @@ var tagVariables = new AliasNameCategoryDescriptor( Mutation calls are gated on a `SecurityAdmin` caller over a `SignAndEncrypt` channel and return `BadUserAccessDenied` otherwise. -It is opt-in: servers that only need `FindAlias` to answer from their -store pay nothing. The created nodes are a snapshot taken at -address-space creation — aliases added or removed later through +Standard-category materialization is opt-in: servers that only need +`FindAlias` to answer from their store do not create alias instance +nodes. The created nodes are a snapshot taken at address-space creation +— aliases added or removed later through `AddAliasesToCategory` / `DeleteAliasesFromCategory` change what `FindAlias` returns and advance `LastChange`, but do not add or remove `AliasNameType` nodes. @@ -484,6 +520,8 @@ while letting PubSub drive the rest. * OPC UA Part 17 specification: https://reference.opcfoundation.org/v105/Core/docs/Part17/ +* [Dependency Injection](DependencyInjection.md#alias-name-stores-and-standard-browse-nodes) + — hosted store registration and standard-category materialization. * `tools/Opc.Ua.SourceGeneration.Core/Design/StandardTypes.xml` — Part 17 type definitions consumed by the source generator. * `tests/Opc.Ua.Server.Tests/AliasNames/` — server-side unit tests. diff --git a/docs/DependencyInjection.md b/docs/DependencyInjection.md index eaab0dc562..571b60868f 100644 --- a/docs/DependencyInjection.md +++ b/docs/DependencyInjection.md @@ -75,6 +75,7 @@ Identity-provider extensions hang off `IOpcUaServerBuilder`, |-----------------------------------------------|--------------------------|----------------------------------| | `AddDefaultIdentityAuthenticators(...)` | server, gds | `OpcUa:Server:Identity:Defaults` | | `AddIdentityAuthenticator()` | server, gds | — | +| `AddIdentityAuthenticator(instance)` / `(factory)` | server | — | | `AddIdentityAugmenter()` | server, gds | — | | `AddGdsApplicationSelfAdminProvider()` | gds | — | | `AddJwtIssuer(...)` | server, gds | `OpcUa:Server:Identity:Issuers[]`| @@ -318,10 +319,13 @@ from the dependency injection surface. ## Server feature -`builder.AddServer(o => …)` registers an OPC UA `StandardServer` as an -`IHostedService` via a private `OpcUaServerHostedService`. Endpoints, -PKI root, security policies, and the application instance certificate -are all set up on host startup. +`builder.AddServer(o => …)` hosts a `DependencyInjectionStandardServer` +via a private `OpcUaServerHostedService`. This implementation derives +from `ReverseConnectServer`, so configured reverse connections work +alongside the dependency-injection hooks without an application server +subclass. The ordinary `StandardServer` and its direct-construction +behavior are unchanged. Endpoints, PKI root, security policies, and the +application instance certificate are all set up on host startup. ```csharp using Microsoft.Extensions.DependencyInjection; @@ -347,8 +351,9 @@ builder.Services await builder.Build().RunAsync(); ``` -`.AddNodeManager()` and `.AddSyncNodeManager()` register the -factory under an `OpcUaServerNodeManagerRegistration` wrapper that is +`.AddNodeManager(...)`, `.AddNodeManagers(...)`, and +`.AddSyncNodeManager()` register factories under an +`OpcUaServerNodeManagerRegistration` wrapper that is **scoped to the regular server feature**. Node managers registered this way are **not** visible to the GDS / LDS hosted services running in the same container. See *Combined hosts* below. @@ -381,7 +386,7 @@ Advanced server services can be supplied through the same fluent builder: ```csharp builder.Services .AddOpcUa() - .AddServer(o => /* … */) // optional StandardServer subclass + .AddServer(o => o.ApplicationName = "MyServer") .AddSessionManager() .AddSubscriptionManager() .AddDurableSubscriptions(subscriptionStore, monitoredItemQueueFactory) @@ -397,6 +402,188 @@ an anonymous authenticator matching its default anonymous user-token policy. If a non-anonymous user-token policy is configured without a corresponding authenticator, startup logs a warning. +The customization extensions below are methods on `IOpcUaServerBuilder` +in `Microsoft.Extensions.DependencyInjection`; they return the same builder. +They use the existing server and dependency-injection dependencies. + +Adopting these hooks is optional. Existing direct server construction and +subclass-based customization remain supported; upgrading does not require +switching to dependency injection. + +### Server metadata + +`ConfigureServerProperties(Action)` configures +the metadata published in the server's build information: + +```csharp +services.AddOpcUa() + .AddServer(options => + { + options.ApplicationName = "PlantServer"; + options.ProductUri = "urn:example:PlantServer"; + }) + .ConfigureServerProperties(properties => + { + properties.ManufacturerName = "Example Automation"; + properties.SoftwareVersion = "2.0.0"; + properties.BuildNumber = "42"; + properties.BuildDate = new DateTime(2026, 9, 1, 0, 0, 0, DateTimeKind.Utc); + }); +``` + +The callback configures `IOptions`. A directly +registered `Opc.Ua.ServerProperties` service takes precedence over those +options; the two sources are not merged. + +| Property left unspecified | Default | +|---------------------------|---------| +| `ProductName` | The effective `ApplicationConfiguration.ApplicationName`. | +| `ProductUri` | The effective `ApplicationConfiguration.ProductUri`. | +| `SoftwareVersion` | The existing stack software-version helper's value. | +| `BuildNumber` | The existing stack build-number helper's value. | +| `ManufacturerName`, `BuildDate` | No publisher identity or build date is inferred; configure these explicitly. | + +The version and build-number fallbacks are **stack defaults**, not the +publisher application's version. Supply the publisher's own metadata for +released products. On the XML/stream path, the effective application +identity comes from the loaded configuration, including any +`ConfigureLoadedConfiguration` changes. + +### Node-manager factories + +Use `AddNodeManager()` for an `IAsyncNodeManagerFactory` resolved +from dependency injection, or the existing `AddSyncNodeManager()` +for an `INodeManagerFactory`. Generic factories are resolved lazily, after +the effective application configuration has been loaded or built. +Already-constructed instances are also supported through +`AddNodeManager(IAsyncNodeManagerFactory)` and +`AddNodeManager(INodeManagerFactory)`. + +Configuration-dependent constructors should use +`AddNodeManagers(Func>)`. +The callback receives the effective configuration, including loaded-file +overrides, and can return zero, one, or many factories: + +```csharp +using Microsoft.Extensions.DependencyInjection; + +services.AddOpcUa() + .AddServer("PlantServer.Config.xml") + .AddNodeManagers((sp, configuration) => + [ + ActivatorUtilities.CreateInstance(sp, configuration) + ]); +``` + +Here `PlantNodeManagerFactory` is an application-defined +`IAsyncNodeManagerFactory` whose constructor accepts +`ApplicationConfiguration` and any other injected dependencies. Use `[]` +when no factories are needed, or include more factories in the returned +`ArrayOf`. The loaded configuration is supplied +explicitly; it is **not** registered as a global `ApplicationConfiguration` +service for constructor injection. + +### Post-startup tasks + +`AddStartupTask()`, where `TTask : class, IServerStartupTask`, +registers an injected task implementing +`OnServerStartedAsync(IServerContext, CancellationToken)`. Alternatively, +`AddStartupTask(Func)` +registers an asynchronous delegate. `IServerStartupTask` is in +`Opc.Ua.Server.Hosting`. + +For example, register an application task, or delegate to an injected +initializer whose `InitializeAsync` returns `ValueTask`: + +```csharp +serverBuilder.AddStartupTask(); +serverBuilder.AddStartupTask((sp, context, cancellationToken) => + sp.GetRequiredService().InitializeAsync(context, cancellationToken)); +``` + +Type registration is idempotent: registering the same `TTask` again does +not run it twice. Separate delegate registrations are distinct tasks. +Tasks run sequentially in registration order, once after the server has +started. An exception or cancellation aborts hosted startup and triggers +server cleanup; subsequent tasks do not run. + +`StopAsync` waits for execution cleanup even after a host shutdown timeout, +so the host does not dispose dependencies that the server is still using. +Slow cleanup can therefore outlast that timeout. + +Use injected services for application dependencies and the existing typed +node-manager lookup for server subsystems. For example, inside a task: + +```csharp +using System.Linq; +using Opc.Ua; +using Opc.Ua.Server; + +IDiagnosticsNodeManager diagnostics = + context.FindNodeManagers().Single(); +HistoryServerCapabilitiesState capabilities = + await diagnostics.GetDefaultHistoryCapabilitiesAsync(cancellationToken) + .ConfigureAwait(false); +``` + +No extra `IServerContext` members or server subclass are needed. Prefer +the historian providers' automatic +[capabilities rollup](HistoricalAccess.md#capability-discovery) when it +already describes the server's history support, rather than manually +overriding the published capabilities in a task. + +### Alias-name stores and standard browse nodes + +`AddAliasNameStore(store)` and `AddAliasNameStoreRegistry(registry)` +make registered stores available before address-space startup. +`ConfigureAliasNames(Action)` controls whether +the normal `ConfigurationNodeManager` also materializes their aliases +and declared optional capabilities under the standard `TagVariables` +and `Topics` categories: + +```csharp +serverBuilder + .AddAliasNameStore(aliasNameStore) + .ConfigureAliasNames(options => options.MaterializeAliasNodes = true); +``` + +`AliasNameServerOptions` is in `Opc.Ua.Server.AliasNames`; its +`MaterializeAliasNodes` defaults to `false`. Materialized browse nodes +are a startup snapshot, not a live mirror of store mutations. +This setting is independent of `AliasNameNodeManagerOptions` for custom +categories, whose `MaterializeAliasNodes` default remains `true`. +See [Alias Names](AliasNames.md#browsable-alias-nodes) for capabilities, +mutation behavior, and the lower-level materialization helper. + +### Localized resources + +Use `ConfigureResources(Action)` or +`ConfigureResources(Action)` +to add translations or replace default status text. The callbacks run +after the base resource manager has loaded the stack's default status +text. In a Generic Host, the service-provider overload can also consume +application configuration: + +```csharp +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +serverBuilder + .ConfigureResources(resources => + resources.Add("DeviceUnavailable", "de-DE", "Das Gerät ist nicht verfügbar.")) + .ConfigureResources((sp, resources) => + { + string text = sp.GetRequiredService()["Messages:DeviceUnavailable"] + ?? "The device is unavailable."; + resources.Add("DeviceUnavailable", "en-US", text); + }); +``` + +The server creates, retains, and disposes its own resource manager. +These callbacks do not register a live `Opc.Ua.Server.ResourceManager` +service for injection. Resolve supporting services from `sp` and use the +manager passed to the callback. + ### Migrating with an existing configuration XML file Applications that already own a classic OPC UA application configuration @@ -428,15 +615,19 @@ builder.Services.AddOpcUa().AddServer("MyServer.Config.xml"); On this path the file is authoritative: the `OpcUaServerOptions` knobs that feed the configuration builder (`ApplicationName`, `EndpointUrls`, -`PkiRoot`, policy toggles, transport quotas, `ConfigureBuilder`, ...) +`PkiRoot`, policy toggles, transport quotas, `ReverseConnect`, +`ConfigureBuilder`, ...) are not applied, and the file also takes precedence over a shared application registered with `ConfigureApplication(...)`. Options that act on the hosted server itself (`Identity`, `ConfigureRateLimits`) and -all fluent registrations (`AddNodeManager`, `ConfigureRoles`, -`AddDefaultIdentityAuthenticators`, ...) keep working. To override -individual file settings from code, pass the optional callback, which -runs after the file is loaded and validated but before certificates are -checked and the server starts: +runtime registrations (`AddNodeManager`, `AddNodeManagers`, startup tasks, +authenticators, metadata, resources, and alias settings) keep working. +Configuration-building shortcuts such as `AddReverseConnect` and +`ConfigureOperationLimits` do not override file settings. To override +individual file settings from code, use `ConfigureLoadedConfiguration`, +also exposed as the optional `AddServer` callback below. It runs after +the file is loaded and validated but before certificates are checked +and the server starts: ```csharp builder.Services @@ -580,8 +771,12 @@ properties (bindable from `IConfiguration` or set via the ### Server-side reverse connect -A server can dial back to clients via reverse-hello using -`OpcUaServerOptions.ReverseConnect`. The data binds directly from +A regular DI server can dial back to clients via reverse-hello using +`OpcUaServerOptions.ReverseConnect` or the fluent `AddReverseConnect(...)` +shortcut. Its `ReverseConnectServer` base runs the configured outbound +connections while retaining the other DI customization hooks. +The options projection into `ServerConfiguration.ReverseConnect` is +unchanged. The data binds directly from `OpcUa:Server:ReverseConnect`: ```jsonc @@ -620,6 +815,13 @@ services.AddOpcUa().AddServer(o => }); ``` +For a loaded XML file or stream, its `ServerConfiguration.ReverseConnect` +is authoritative; use `ConfigureLoadedConfiguration` to change it rather +than the options-building shortcuts. Directly constructed `StandardServer` +instances do not gain reverse-connect behavior. See +[Reverse Connect](ReverseConnect.md#server-side-dependency-injection) +for the hosted lifecycle and a loaded-configuration example. + ### Operation limits ```csharp @@ -670,6 +872,8 @@ the hosted service falls back to a single `Anonymous` policy. |---|---| | `ConfigureRoles(Action)` / `(IConfiguration)` | Registers `RoleConfigurationOptions` for future role-related tuning. The DTO currently has no configurable members; the extension exists as a stable expansion point. | | `AddIdentityAuthenticator()` | Registers a single custom `IUserTokenAuthenticator` implementation. The hosted service adds it to `IServerInternal.IdentityRegistry` on startup. | +| `AddIdentityAuthenticator(IUserTokenAuthenticator)` | Registers an already-constructed authenticator instance. | +| `AddIdentityAuthenticator(Func)` | Creates an authenticator during startup with injected services and the effective certificate validator, which may be `null`. | | `AddIdentityAugmenter()` | Registers a single custom `IIdentityAugmenter` implementation. The hosted service runs it after accepted authentications. | | `AddDefaultIdentityAuthenticators(Action)` / `(IConfiguration)` | Registers the four in-box authenticators (Anonymous, UserNamePassword, X509, Jwt) with toggles per type plus the JWT audience / clock-skew settings. | | `AddJwtIssuer(Action)` / `(IConfiguration)` | Registers a trusted JWT issuer. Multiple calls coexist; each contributes a `StaticIssuerKeyResolver` and / or `JwksIssuerKeyResolver` keyed by `IssuerUri`. | @@ -719,6 +923,15 @@ hosted service skips that authenticator if neither is in dependency injection. S [Role-Based Security](RoleBasedUserManagement.md) for the role-mapping layer. +The instance and factory overloads complement the existing generic +registration. Factories receive the validator after configuration and +certificate setup; a validator-dependent authenticator should fail +clearly if the nullable validator is absent. Register matching +`UserTokenPolicies` as well — an authenticator does not advertise its +token policy automatically. See +[custom authenticator registrations](IdentityProviders.md#custom-authenticator-registrations) +for an example using the configured certificate stores. + ### Fluent shortcuts and one-shot presets The server hosting surface exposes granular extension points and one-shot presets: @@ -756,8 +969,13 @@ services.AddOpcUa() ``` Use `AddRoleManager(IRoleManager)` or `AddRoleManager()` to replace the default role manager. -Custom server types that need session, subscription, or durable-subscription DI hooks must derive from -`DependencyInjectionStandardServer`; otherwise startup fails fast instead of ignoring those hooks. +`AddServer()` remains available for custom server implementations. +Custom types that need session, subscription, or durable-subscription DI +hooks must derive from `DependencyInjectionStandardServer`. The same +requirement applies to DI-only metadata, resource, and alias settings +configured through `ConfigureServerProperties`, `ConfigureResources`, +and `ConfigureAliasNames`. A non-DI custom server rejects these settings +clearly during startup instead of silently ignoring them. Fluent node managers can be registered without a factory class: diff --git a/docs/IdentityProviders.md b/docs/IdentityProviders.md index c2d52d189d..0c792140f0 100644 --- a/docs/IdentityProviders.md +++ b/docs/IdentityProviders.md @@ -107,6 +107,61 @@ enables the configured-default-authenticators bridge: > `AddDefaultIdentityAuthenticators(opt => …)` and `AddJwtIssuer(opt => …)` > explicitly. +### Custom authenticator registrations + +`AddIdentityAuthenticator()` resolves an authenticator from +dependency injection. Two other extensions on `IOpcUaServerBuilder`, +in `Microsoft.Extensions.DependencyInjection`, support already-created +instances and configuration-aware construction: + +| Overload | Use | +|----------|-----| +| `AddIdentityAuthenticator(IUserTokenAuthenticator)` | Register an existing authenticator instance. | +| `AddIdentityAuthenticator(Func)` | Construct an authenticator during startup using injected services and the effective certificate validator. | + +`IUserTokenAuthenticator` is in `Opc.Ua.Identity`; +`ICertificateValidatorEx` is in `Opc.Ua`. +The factory runs after configuration and certificate setup, including +when configuration is loaded from XML or a stream. Its validator can be +`null`: an authenticator that requires it should reject that case +explicitly instead of bypassing validation. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Opc.Ua; +using Opc.Ua.Server.Hosting; + +services.AddOpcUa() + .AddServer(options => + { + options.ApplicationName = "CertificateServer"; + options.ApplicationUri = "urn:example:CertificateServer"; + options.UserTokenPolicies.Add(new OpcUaUserTokenPolicy + { + TokenType = UserTokenType.Anonymous + }); + options.UserTokenPolicies.Add(new OpcUaUserTokenPolicy + { + TokenType = UserTokenType.Certificate + }); + }) + .AddIdentityAuthenticator(new Opc.Ua.Server.AnonymousAuthenticator()) + .AddIdentityAuthenticator((_, validator) => + new Opc.Ua.Server.X509Authenticator( + validator ?? throw new InvalidOperationException( + "A certificate validator is required for X.509 user tokens."))); +``` + +Provision user-certificate trust through the +[certificate manager and stores](CertificateManager.md); no certificate +or secret belongs in this registration code. Register matching +`UserTokenPolicies` as shown: adding an authenticator does not +automatically advertise its token policy. Use the service-provider +argument when the factory also needs application services. + +The instance and factory overloads are on `IOpcUaServerBuilder`; the +existing generic registration remains available on the GDS builder. + ### Client example The composite builder API takes the supporting service (an @@ -188,7 +243,7 @@ is registered. ### GDS example -`IGdsServerBuilder` forwards every identity-related extension to the +`IGdsServerBuilder` forwards the shared identity configuration extensions to the underlying server builder (`ConfigureRoles`, `AddIdentityAuthenticator`, `AddIdentityAugmenter`, `AddDefaultIdentityAuthenticators`, `AddJwtIssuer` — each with both diff --git a/docs/ReverseConnect.md b/docs/ReverseConnect.md index b6b326677a..b76014d78b 100644 --- a/docs/ReverseConnect.md +++ b/docs/ReverseConnect.md @@ -5,11 +5,11 @@ The Reverse Connect option consists of the following elements: * Updated C# Stack that supports the *ReverseHello* message for Client and Server; -* Updated server library which support to - * Create a *ReverseConnectServer* derived from a *StandardServer* class. - * Extended configuration parameters to setup the client location and timeouts. - * an API extension in the *ReverseConnectServer* to programmatically control client connections. -* Updated client library which support to +* Updated server library which supports: + * Server-initiated connections through *ReverseConnectServer*, used automatically by the regular dependency-injection server. + * Extended configuration parameters to set up the client location and timeouts. + * An API extension in the *ReverseConnectServer* to programmatically control client connections. +* Updated client library which supports: * Configure a client endpoint to accept *ReverseHello* messages using a *ReverseConnectManager*. * A client API extension to allow applications to register for reverse connections either by callback or by waiting for the *ReverseHello* message for a specific server endpoint and application Uri combination. An optional filter for server Uris or endpoint Urls can be applied to allow multiple clients to use the same endpoint. * The C# [Console Reference Server](../samples/Reference/ConsoleReferenceServer) with reverse connect support in the configuration xml. @@ -73,6 +73,72 @@ Pass each returned `ITransportWaitingConnection` to the session factory for the The synchronous `StartService`, `RegisterWaitingConnection`, and `Dispose` APIs remain as obsolete compatibility wrappers. New code should use `StartServiceAsync`, `RegisterWaitingConnectionAsync`, and `DisposeAsync`; the compatibility wrappers may block a caller thread. +## Server-side dependency injection + +The regular `AddServer(...)` host uses +`DependencyInjectionStandardServer`, which derives from +`ReverseConnectServer`. Configured outbound reverse connections start +and stop with the hosted server. Existing DI hooks for node managers, +session and subscription services, identity, and startup tasks coexist +with reverse connect; an application does not need a server subclass. +Queued callbacks from disposed or superseded retry timers are ignored, +including during startup-failure cleanup. + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Opc.Ua.Server.Hosting; + +services.AddOpcUa() + .AddServer(options => + { + options.ApplicationName = "PlantServer"; + options.EndpointUrls.Add("opc.tcp://localhost:4840/PlantServer"); + }) + .AddReverseConnect(options => + { + options.ConnectIntervalMs = 15000; + options.Clients.Add(new ServerReverseConnectClientOptions + { + EndpointUrl = "opc.tcp://client.example.com:65300", + Timeout = 30000, + MaxSessionCount = 1, + Enabled = true + }); + }); +``` + +`AddReverseConnect(...)` configures `OpcUaServerOptions.ReverseConnect`. +The projection into `ServerConfiguration.ReverseConnect` is unchanged, +and the same options can be bound from `OpcUa:Server:ReverseConnect`. + +When `ConfigurationFile` or `ConfigurationStream` supplies the +application configuration, that document is authoritative. The +`ReverseConnect` options and `AddReverseConnect(...)` shortcut do not +overwrite it. Use `ConfigureLoadedConfiguration` to change loaded +settings; this example retains the file's clients and changes only +their retry interval: + +```csharp +services.AddOpcUa().AddServer(options => +{ + options.ConfigurationFile = "PlantServer.Config.xml"; + options.ConfigureLoadedConfiguration = configuration => + { + var reverseConnect = configuration.ServerConfiguration.ReverseConnect + ?? throw new InvalidOperationException( + "Configure reverse-connect clients in the XML file."); + reverseConnect.ConnectInterval = 20000; + }; +}); +``` + +The ordinary `StandardServer` is unchanged. Applications constructing +servers manually still choose `ReverseConnectServer` explicitly when +they need server-initiated connections; setting reverse-connect +configuration alone does not add that behavior to `StandardServer`. +See [Dependency Injection](DependencyInjection.md#server-side-reverse-connect) +for the complete options shape and other hosted-server customization. + ## Dependency-injection lifecycle `AddClient(...)` registers one singleton `ReverseConnectManager`, an `IReverseConnectConfigurationProvider`, and an internal hosted service. In a .NET Generic Host, the hosted service eagerly opens configured listeners during host startup and closes them during host shutdown. In a plain `ServiceCollection` without a running host, `WaitForConnectionAsync` and `RegisterWaitingConnectionAsync` call `EnsureStartedAsync` lazily. Resolving the manager itself never blocks on listener startup. diff --git a/src/Opc.Ua.Core.Diagnostics/Opc.Ua.Core.Diagnostics.csproj b/src/Opc.Ua.Core.Diagnostics/Opc.Ua.Core.Diagnostics.csproj index 758d688002..d8ad05426a 100644 --- a/src/Opc.Ua.Core.Diagnostics/Opc.Ua.Core.Diagnostics.csproj +++ b/src/Opc.Ua.Core.Diagnostics/Opc.Ua.Core.Diagnostics.csproj @@ -26,7 +26,7 @@ enable disable true - true + true - net8.0 + $(TestsTargetFrameworks) false