diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md
index 16db44b205..40033939c4 100644
--- a/docs/MigrationGuide.md
+++ b/docs/MigrationGuide.md
@@ -196,6 +196,27 @@ for the before/after and
[Custom node types and assignment control](NodeManagers.md#custom-node-types-and-assignment-control)
for the runtime rules.
+## Removed members on IServerInternal
+
+The following members had no consumer anywhere in the stack, its samples or
+its tests, and have been removed. Each has a direct replacement:
+
+| Removed | Use instead |
+| --- | --- |
+| `CloseSession(OperationContext, NodeId, bool)` | `CloseSessionAsync(…)` |
+| `Status` | `CurrentState` to read, `UpdateServerStatus` to write |
+| `ServerDiagnostics` | `UpdateServerDiagnostics(Action<…>)` |
+| `DiagnosticsEnabled` | `IDiagnosticsNodeManager.DiagnosticsEnabled` |
+| `ModellingRulesManager`, `ConformanceUnitsManager` | the concrete `ServerInternalData`, which owns them |
+
+`MessageContext`, `DefaultSystemContext`, `CurrentState`, `ServerObject`,
+`ReportEventAsync`, `CloseSessionAsync`, `DeleteSubscriptionAsync` and
+`UpdateServerDiagnostics` all moved *down* to `IServerContext`, the ambient
+view of a running server that `IServerInternal` now derives from. They remain
+reachable through `IServerInternal` unchanged, so no call site has to move.
+`CurrentState` is read-only on the ambient interface; the server itself still
+sets it.
+
## Migrating servers that relied on unserved history advertisement
Server startup now reconciles variables that advertise history with the
diff --git a/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs b/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs
index 1cd804c9b5..807e263cfc 100644
--- a/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Kubernetes/Health/KubernetesReadinessStartupTask.cs
@@ -50,7 +50,7 @@ namespace Opc.Ua.Redundancy.Kubernetes
}
///
- public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
if (server == null)
{
diff --git a/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs
index 75015c11a6..fc1b0afd00 100644
--- a/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/Discovery/PeerDiscoveryStartupTask.cs
@@ -64,14 +64,14 @@ namespace Opc.Ua.Redundancy.Server
}
///
- public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}
- m_logger = server.Telemetry.CreateLogger();
+ m_logger = server.DefaultSystemContext.Telemetry.CreateLogger();
m_discovery.PeersChanged += OnPeersChanged;
m_loop = Task.Run(() => RefreshLoopAsync(m_cts.Token), CancellationToken.None);
return default;
diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs
index 7cb1e01155..9d15994238 100644
--- a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/LoadDirectionStartupTask.cs
@@ -66,18 +66,18 @@ namespace Opc.Ua.Redundancy.Server
}
///
- public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}
- string[] serverUris = server.ServerUris.ToArray();
+ string[] serverUris = server.DefaultSystemContext.ServerUris.ToArray();
string? localServerUri = serverUris.Length > 0 ? serverUris[0] : null;
if (string.IsNullOrEmpty(localServerUri))
{
- server.Telemetry
+ server.DefaultSystemContext.Telemetry
.CreateLogger()
.LoadDirectionDisabledLocalServerUriUnavailable();
return default;
diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs
index a7b8c846ed..35d53a80f8 100644
--- a/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/Redundancy/LoadDirection/PeerDirectionPublishStartupTask.cs
@@ -71,8 +71,7 @@ namespace Opc.Ua.Redundancy.Server
}
///
- public async ValueTask OnServerStartedAsync(
- IServerInternal server,
+ public async ValueTask OnServerStartedAsync(IServerContext server,
CancellationToken cancellationToken = default)
{
if (server == null)
@@ -81,7 +80,7 @@ namespace Opc.Ua.Redundancy.Server
}
string? localServerUri = ResolveLocalServerUri(server);
- m_logger = server.Telemetry.CreateLogger();
+ m_logger = server.DefaultSystemContext.Telemetry.CreateLogger();
if (string.IsNullOrEmpty(localServerUri))
{
m_logger.LoadDirectionDisabledLocalServerUriUnavailableForPeerDirection();
@@ -174,9 +173,9 @@ namespace Opc.Ua.Redundancy.Server
}
}
- private static string? ResolveLocalServerUri(IServerInternal server)
+ private static string? ResolveLocalServerUri(IServerContext server)
{
- string[] serverUris = server.ServerUris.ToArray();
+ string[] serverUris = server.DefaultSystemContext.ServerUris.ToArray();
return serverUris.Length > 0 ? serverUris[0] : null;
}
diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs
index dd4d04c58d..31ced16b38 100644
--- a/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/Redundancy/RequestServerStateChangeStartupTask.cs
@@ -28,6 +28,7 @@
* ======================================================================*/
using System;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.Server;
@@ -64,7 +65,7 @@ namespace Opc.Ua.Redundancy.Server
}
///
- public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
if (server == null)
{
@@ -79,7 +80,7 @@ namespace Opc.Ua.Redundancy.Server
m_server = server;
RequestServerStateChangeMethodState? requestServerStateChange =
- server.DiagnosticsNodeManager?.FindPredefinedNode(
+ server.FindPredefinedNode(
MethodIds.Server_RequestServerStateChange) ??
serverObject.RequestServerStateChange;
requestServerStateChange?.OnCall = OnRequestServerStateChange;
@@ -101,7 +102,7 @@ namespace Opc.Ua.Redundancy.Server
{
ValidateAdminAccess(context);
- IServerInternal? server = m_server;
+ IServerContext? server = m_server;
ServerObjectState? serverObject = server?.ServerObject;
if (server == null || serverObject == null)
{
@@ -128,7 +129,7 @@ namespace Opc.Ua.Redundancy.Server
return;
}
- IConfigurationNodeManager? configurationNodeManager = (m_server?.ConfigurationNodeManager) ??
+ IConfigurationNodeManager? configurationNodeManager = m_server?.FindNodeManagers().FirstOrDefault() ??
throw new ServiceResultException(
StatusCodes.BadUserAccessDenied,
"A configuration node manager is required to validate administrator access.");
@@ -200,6 +201,6 @@ namespace Opc.Ua.Redundancy.Server
private readonly RequestServerStateChangeOptions m_options;
private readonly IServiceLevelController? m_serviceLevelController;
- private IServerInternal? m_server;
+ private IServerContext? m_server;
}
}
diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs
index 0f03ed9217..63cf1c6dfa 100644
--- a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs
+++ b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyController.cs
@@ -29,6 +29,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.Server;
@@ -131,7 +132,7 @@ namespace Opc.Ua.Redundancy.Server
/// The cancellation token.
/// is null.
internal async ValueTask AttachAsync(
- IServerInternal server,
+ IServerContext server,
CancellationToken cancellationToken = default)
{
if (server == null)
@@ -167,7 +168,7 @@ namespace Opc.Ua.Redundancy.Server
/// configured mode and publishes the mode-specific values.
///
private static async ValueTask ApplyAsync(
- IServerInternal server,
+ IServerContext server,
ServerRedundancyOptions options,
CancellationToken cancellationToken)
{
@@ -187,7 +188,7 @@ namespace Opc.Ua.Redundancy.Server
}
NodeId targetTypeDefinition = GetTypeDefinitionId(options.Mode);
- var replacer = server.DiagnosticsNodeManager as IPredefinedNodeSubtypeReplacer;
+ IPredefinedNodeSubtypeReplacer? replacer = server.FindNodeManagers().FirstOrDefault();
if (replacer != null && !redundancy.TypeDefinitionId.Equals(targetTypeDefinition))
{
@@ -302,6 +303,6 @@ namespace Opc.Ua.Redundancy.Server
private readonly ServerRedundancyOptions m_options;
private readonly SemaphoreSlim m_gate = new(1, 1);
- private IServerInternal? m_server;
+ private IServerContext? m_server;
}
}
diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs
index a7cb5f9adc..01fcf38def 100644
--- a/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/Redundancy/ServerRedundancyStartupTask.cs
@@ -61,8 +61,7 @@ namespace Opc.Ua.Redundancy.Server
}
///
- public async ValueTask OnServerStartedAsync(
- IServerInternal server,
+ public async ValueTask OnServerStartedAsync(IServerContext server,
CancellationToken cancellationToken = default)
{
if (server == null)
@@ -75,14 +74,14 @@ namespace Opc.Ua.Redundancy.Server
await m_controller.AttachAsync(server, cancellationToken).ConfigureAwait(false);
}
- private void WarnIfServiceLevelProviderMissing(IServerInternal server)
+ private void WarnIfServiceLevelProviderMissing(IServerContext server)
{
if (!m_warnIfServiceLevelProviderMissing || !m_controller.Options.IsNonTransparentMode)
{
return;
}
- ILogger logger = server.Telemetry.CreateLogger();
+ ILogger logger = server.DefaultSystemContext.Telemetry.CreateLogger();
logger.NonTransparentServerRedundancyModeWithoutServiceLevelProvider(m_controller.Mode);
}
diff --git a/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs b/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs
index d5dbaa351d..a172504fa3 100644
--- a/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/Redundancy/ServiceLevelStartupTask.cs
@@ -56,7 +56,7 @@ namespace Opc.Ua.Redundancy.Server
}
///
- public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
if (server == null)
{
diff --git a/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs b/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs
index 53ad7cb3b4..f14626f8a3 100644
--- a/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/State/DistributedAddressSpaceStartupTask.cs
@@ -77,14 +77,14 @@ namespace Opc.Ua.Redundancy.Server
public INodeStateStoreRegistry? NodeStateStoreRegistry { get; private set; }
///
- public async ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public async ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}
- ILogger logger = server.Telemetry.CreateLogger();
+ ILogger logger = server.DefaultSystemContext.Telemetry.CreateLogger();
// Build the store with the server's populated message context so
// NodeId namespace indices resolve correctly. Disposal ownership is
@@ -97,7 +97,7 @@ namespace Opc.Ua.Redundancy.Server
// Own the node state store registry; nothing in the core server
// surface holds it. The default store is the fallback for every
// node that does not have a more specific binding.
- var registry = new NodeStateStoreRegistry(server.NamespaceUris);
+ var registry = new NodeStateStoreRegistry(server.DefaultSystemContext.NamespaceUris);
registry.RegisterDefault(store);
m_registry = registry;
NodeStateStoreRegistry = registry;
@@ -107,13 +107,9 @@ namespace Opc.Ua.Redundancy.Server
await m_election.TryAcquireOrRenewAsync(cancellationToken).ConfigureAwait(false);
m_election.Start();
- foreach (INodeManager nodeManager in server.NodeManager.NodeManagers)
+ foreach (ILocalAddressSpaceSource source in
+ server.FindNodeManagers())
{
- if (nodeManager is not ILocalAddressSpaceSource source)
- {
- continue;
- }
-
ILocalAddressSpace addressSpace = source.CreateLocalAddressSpace();
var synchronizer = new AddressSpaceSynchronizer(
store, addressSpace, m_election, logger);
diff --git a/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs b/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs
index 169c40717f..4cc621ebe4 100644
--- a/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs
+++ b/src/Opc.Ua.Redundancy.Server/State/ReplicatedAddressSpaceStartupTask.cs
@@ -58,22 +58,18 @@ namespace Opc.Ua.Redundancy.Server
}
///
- public async ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public async ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
if (server == null)
{
throw new ArgumentNullException(nameof(server));
}
- ILogger logger = server.Telemetry.CreateLogger();
+ ILogger logger = server.DefaultSystemContext.Telemetry.CreateLogger();
- foreach (INodeManager nodeManager in server.NodeManager.NodeManagers)
+ foreach (ILocalAddressSpaceSource source in
+ server.FindNodeManagers())
{
- if (nodeManager is not ILocalAddressSpaceSource source)
- {
- continue;
- }
-
ILocalAddressSpace addressSpace = source.CreateLocalAddressSpace();
ITransport transport = m_options.CreateTransport(m_services, out InMemoryNetwork? defaultNetwork);
if (defaultNetwork != null)
diff --git a/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs b/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs
index 18a172279c..d72f2f8d8c 100644
--- a/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs
+++ b/src/Opc.Ua.Robotics.Server/Hosting/RobotIntentPostSetup.cs
@@ -99,16 +99,13 @@ namespace Opc.Ua.Robotics.Server.Hosting
internal sealed class RobotIntentHostStartupTask : IServerStartupTask
{
public ValueTask OnServerStartedAsync(
- IServerInternal server,
+ IServerContext server,
CancellationToken cancellationToken = default)
{
- IReadOnlyList nodeManagers = server.NodeManager.AsyncNodeManagers;
- for (int ii = 0; ii < nodeManagers.Count; ii++)
+ foreach (RobotIntentNodeManager robotIntentNodeManager in
+ server.FindNodeManagers())
{
- if (nodeManagers[ii] is RobotIntentNodeManager robotIntentNodeManager)
- {
- robotIntentNodeManager.StartIntentControllerHosts();
- }
+ robotIntentNodeManager.StartIntentControllerHosts();
}
return default;
}
diff --git a/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs b/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs
index 5897d4c4f6..06fe4d5340 100644
--- a/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs
+++ b/src/Opc.Ua.Server/Hosting/IServerStartupTask.cs
@@ -34,20 +34,24 @@ namespace Opc.Ua.Server.Hosting
{
///
/// A task run by the hosted server immediately after the server has
- /// started, with access to the live . Every
+ /// started, with access to the live . Every
/// implementation registered in DI is invoked once. This is the seam
/// features use to wire runtime behavior that needs the fully-initialized
- /// server (loaded node managers, the populated message context, the
- /// ServerObject, the session manager) without subclassing
- /// .
+ /// server (a bound address space, the populated message context, the
+ /// ServerObject) without subclassing .
///
+ ///
+ /// A task that needs a particular subsystem takes it as a constructor
+ /// dependency, which is how every implementation in this repository already
+ /// obtains what it needs. The context carries only what is genuinely ambient.
+ ///
public interface IServerStartupTask
{
///
/// Invoked once after the server has started.
///
- /// The live server internals.
+ /// The live server context.
/// Cancellation token.
- ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default);
+ ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default);
}
}
diff --git a/src/Opc.Ua.Server/Server/IServerContext.cs b/src/Opc.Ua.Server/Server/IServerContext.cs
new file mode 100644
index 0000000000..6f71fb0187
--- /dev/null
+++ b/src/Opc.Ua.Server/Server/IServerContext.cs
@@ -0,0 +1,153 @@
+/* ========================================================================
+ * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
+ *
+ * OPC Foundation MIT License 1.00
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * The complete license agreement can be found here:
+ * http://opcfoundation.org/License/MIT/1.00/
+ * ======================================================================*/
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Opc.Ua.Server
+{
+ ///
+ /// The ambient view of a running server, handed to components that need to observe it
+ /// and act on it without owning any part of it.
+ ///
+ ///
+ ///
+ /// This is deliberately small. Every member either answers a question about the server
+ /// as a whole or performs a server-wide operation; none of them hands out a subsystem.
+ /// A component that needs a particular subsystem asks for that subsystem in its
+ /// constructor, which states the dependency instead of hiding it behind an ambient
+ /// handle.
+ ///
+ ///
+ /// The ambient tables - namespace URIs, server URIs, the type table, the encodeable
+ /// factory and telemetry - are reached through ,
+ /// which already carries all of them. They are not repeated here.
+ ///
+ ///
+ public interface IServerContext
+ {
+ ///
+ /// The current state of the server.
+ ///
+ ServerState CurrentState { get; }
+
+ ///
+ /// The Server object node.
+ ///
+ ServerObjectState ServerObject { get; }
+
+ ///
+ /// The context to use when serializing and deserializing extension objects.
+ ///
+ ///
+ /// This is the server's own context and carries its configured decoding limits.
+ /// It is deliberately not derived from :
+ /// ISystemContext.AsMessageContext() produces a context with default
+ /// limits, which would silently widen what a component accepts.
+ ///
+ IServiceMessageContext MessageContext { get; }
+
+ ///
+ /// The context to use for operations that are not attributable to a session.
+ /// Also carries the namespace URIs, server URIs, type table, encodeable factory
+ /// and telemetry context.
+ ///
+ ServerSystemContext DefaultSystemContext { get; }
+
+ ///
+ /// Creates a context that attributes operations to the given session, carrying its
+ /// identity and preferred locales.
+ ///
+ /// The session to attribute operations to.
+ ServerSystemContext CreateSystemContext(ISession session);
+
+ ///
+ /// Finds a node in the server's predefined address space.
+ ///
+ /// The expected node type.
+ /// The node to find.
+ ///
+ /// The node, or null when it is absent or is not of type
+ /// .
+ ///
+ T? FindPredefinedNode(NodeId nodeId) where T : NodeState;
+
+ ///
+ /// Finds every registered node manager that provides the requested capability.
+ ///
+ ///
+ /// Node managers advertise optional capabilities by implementing marker interfaces.
+ /// This asks for the capability rather than for the registry, so callers do not
+ /// have to know how node managers are stored or filter the list themselves.
+ ///
+ /// The capability to look for.
+ IEnumerable FindNodeManagers() where T : class;
+
+ ///
+ /// Reports a global event.
+ ///
+ /// The event.
+ /// The cancellation token.
+ ValueTask ReportEventAsync(IFilterTarget e, CancellationToken cancellationToken = default);
+
+ ///
+ /// Closes the specified session.
+ ///
+ /// The context.
+ /// The session identifier.
+ /// if set to true subscriptions are to be deleted.
+ /// The cancellation token.
+ ValueTask CloseSessionAsync(
+ OperationContext context,
+ NodeId sessionId,
+ bool deleteSubscriptions,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Deletes the specified subscription.
+ ///
+ /// The subscription identifier.
+ /// The cancellation token.
+ ValueTask DeleteSubscriptionAsync(uint subscriptionId, CancellationToken cancellationToken = default);
+
+ ///
+ /// Applies an update to the server diagnostics while holding the server's
+ /// diagnostics lock.
+ ///
+ ///
+ /// The server owns its lock and never exposes it, so callers cannot participate in
+ /// the server's locking order. The diagnostic nodes are marked dirty inside the
+ /// critical section.
+ ///
+ /// The mutation to apply to the diagnostics.
+ void UpdateServerDiagnostics(Action update);
+ }
+}
diff --git a/src/Opc.Ua.Server/Server/IServerInternal.cs b/src/Opc.Ua.Server/Server/IServerInternal.cs
index 502f20fc70..e5cff3c12f 100644
--- a/src/Opc.Ua.Server/Server/IServerInternal.cs
+++ b/src/Opc.Ua.Server/Server/IServerInternal.cs
@@ -38,7 +38,13 @@ namespace Opc.Ua.Server
///
/// The interface that a server exposes to objects that it contains.
///
- public interface IServerInternal : IAuditEventServer, IDisposable
+ ///
+ /// Components that only need to observe the server and act on it should take
+ /// instead. This interface additionally hands out the
+ /// server's subsystems, which a component should ask for by constructor injection
+ /// rather than reach for through an ambient handle.
+ ///
+ public interface IServerInternal : IServerContext, IAuditEventServer, IDisposable
{
///
/// The endpoint addresses used by the server.
@@ -46,18 +52,6 @@ namespace Opc.Ua.Server
/// The endpoint addresses.
IEnumerable EndpointAddresses { get; }
- ///
- /// The context to use when serializing/deserializing extension objects.
- ///
- /// The message context.
- IServiceMessageContext MessageContext { get; }
-
- ///
- /// The default system context for the server.
- ///
- /// The default system context.
- ServerSystemContext DefaultSystemContext { get; }
-
///
/// The table of namespace uris known to the server.
///
@@ -141,20 +135,6 @@ namespace Opc.Ua.Server
/// The aggregate manager.
AggregateManager AggregateManager { get; }
- ///
- /// A manager for modelling rules supported by the server.
- ///
- /// The modelling rules manager.
- ModellingRulesManager ModellingRulesManager { get; }
-
- ///
- /// A manager for the conformance units and server profiles the server
- /// advertises, aggregated from the registered
- /// node managers.
- ///
- /// The conformance units manager.
- ConformanceUnitsManager ConformanceUnitsManager { get; }
-
///
/// The manager for active sessions.
///
@@ -218,91 +198,6 @@ namespace Opc.Ua.Server
///
bool IsRunning { get; }
- ///
- /// Returns the status object for the server.
- ///
- /// The status.
- [Obsolete("No longer thread safe. To read the value use CurrentState, to write use UpdateServerStatus.")]
- ServerStatusValue Status { get; }
-
- ///
- /// Gets or sets the current state of the server.
- ///
- /// The state of the current.
- ServerState CurrentState { get; set; }
-
- ///
- /// Returns the Server object node
- ///
- /// The Server object node.
- ServerObjectState ServerObject { get; }
-
- ///
- /// Applies an update to the server diagnostics while holding the server's
- /// diagnostics lock.
- ///
- ///
- /// The server owns its lock and never exposes it, so callers cannot participate in
- /// the server's locking order. The diagnostic nodes are marked dirty inside the
- /// critical section.
- ///
- /// The mutation to apply to the diagnostics.
- void UpdateServerDiagnostics(Action update);
-
- ///
- /// Reads a value derived from the server diagnostics while holding the server's
- /// diagnostics lock.
- ///
- ///
- /// Do not let the diagnostics object escape the callback: once the lock is
- /// released, any field read from it is unsynchronized.
- ///
- /// The type of the value produced.
- /// The projection applied to the diagnostics.
- TResult ReadServerDiagnostics(
- Func read);
-
- ///
- /// Returns the diagnostics structure for the server.
- ///
- /// The server diagnostics.
- ServerDiagnosticsSummaryDataType ServerDiagnostics { get; }
-
- ///
- /// Whether the server is collecting diagnostics.
- ///
- /// true if diagnostics is enabled; otherwise, false.
- bool DiagnosticsEnabled { get; }
-
- ///
- /// Closes the specified session.
- ///
- /// The context.
- /// The session identifier.
- /// if set to true subscriptions are to be deleted.
- [Obsolete("Use CloseSessionAsync instead.")]
- void CloseSession(OperationContext context, NodeId sessionId, bool deleteSubscriptions);
-
- ///
- /// Closes the specified session.
- ///
- /// The context.
- /// The session identifier.
- /// if set to true subscriptions are to be deleted.
- /// The cancellation token.
- ValueTask CloseSessionAsync(
- OperationContext context,
- NodeId sessionId,
- bool deleteSubscriptions,
- CancellationToken cancellationToken = default);
-
- ///
- /// Deletes the specified subscription.
- ///
- /// The subscription identifier.
- /// The cancellation token.
- ValueTask DeleteSubscriptionAsync(uint subscriptionId, CancellationToken cancellationToken = default);
-
///
/// Called by any component to report a global event.
///
@@ -316,14 +211,6 @@ namespace Opc.Ua.Server
/// The event.
void ReportEvent(ISystemContext context, IFilterTarget e);
- ///
- /// Asynchronously reports a global event, awaiting an asynchronous report sink so the caller
- /// is never blocked.
- ///
- /// The event.
- /// The cancellation token.
- ValueTask ReportEventAsync(IFilterTarget e, CancellationToken cancellationToken = default);
-
///
/// Asynchronously reports a global event, awaiting an asynchronous report sink so the caller
/// is never blocked.
diff --git a/src/Opc.Ua.Server/Server/ServerInternalData.cs b/src/Opc.Ua.Server/Server/ServerInternalData.cs
index 511b785444..386daa068e 100644
--- a/src/Opc.Ua.Server/Server/ServerInternalData.cs
+++ b/src/Opc.Ua.Server/Server/ServerInternalData.cs
@@ -32,6 +32,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua.Identity;
@@ -493,6 +494,49 @@ namespace Opc.Ua.Server
/// The default system context.
public ServerSystemContext DefaultSystemContext { get; }
+ ///
+ public ServerSystemContext CreateSystemContext(ISession session)
+ {
+ if (session == null)
+ {
+ throw new ArgumentNullException(nameof(session));
+ }
+
+ return DefaultSystemContext.Copy(session);
+ }
+
+ ///
+ public T? FindPredefinedNode(NodeId nodeId) where T : NodeState
+ {
+ return DiagnosticsNodeManager?.FindPredefinedNode(nodeId);
+ }
+
+ ///
+ public IEnumerable FindNodeManagers() where T : class
+ {
+ IMasterNodeManager? nodeManager = NodeManager;
+ if (nodeManager == null)
+ {
+ return [];
+ }
+
+ // A node manager is registered as an asynchronous manager and surfaced again
+ // through a synchronous adapter. A manager that already implements INodeManager
+ // is its own adapter, so the two lists overlap and the capability may sit on
+ // either face; both are searched and the overlap is removed.
+ //
+ // The overlap is the *same instance* appearing twice, so identity is the only
+ // correct comparison. Distinct() would use EqualityComparer.Default and
+ // honour an Equals override on an implementation, silently collapsing two
+ // managers that merely compare equal.
+ List asyncManagers = [.. nodeManager.AsyncNodeManagers.OfType()];
+
+ return asyncManagers.Concat(
+ nodeManager.NodeManagers
+ .OfType()
+ .Where(manager => !asyncManagers.Exists(known => ReferenceEquals(known, manager))));
+ }
+
///
/// The table of namespace uris known to the server.
///
@@ -672,37 +716,11 @@ namespace Opc.Ua.Server
}
}
- ///
- /// Reads a value derived from the server diagnostics while holding the server's
- /// diagnostics lock.
- ///
- ///
- /// Do not let the diagnostics object itself escape the callback: once the lock is
- /// released, any field read from it is unsynchronized.
- ///
- /// The type of the value produced.
- /// The projection applied to the diagnostics.
- /// Thrown if read is null.
- public TResult ReadServerDiagnostics(
- Func read)
- {
- if (read == null)
- {
- throw new ArgumentNullException(nameof(read));
- }
-
- lock (m_diagnosticsLock)
- {
- return read.Invoke(ServerDiagnostics);
- }
- }
-
///
/// Guards the server diagnostics and the server status value, which is constructed
/// with this same lock so the two stay mutually exclusive. Never exposed: callers
- /// reach the diagnostics through and
- /// , and the status through
- /// and .
+ /// reach the diagnostics through , and the
+ /// status through and .
///
private readonly Lock m_diagnosticsLock = new();
@@ -771,22 +789,6 @@ namespace Opc.Ua.Server
///
internal ServerStatusValue NonThreadSafeStatus { get; private set; } = null!;
- ///
- /// Closes the specified session.
- ///
- /// The context.
- /// The session identifier.
- /// if set to true subscriptions are to be deleted.
- [Obsolete("Use CloseSessionAsync instead.")]
- public void CloseSession(
- OperationContext context,
- NodeId sessionId,
- bool deleteSubscriptions)
- {
- CloseSessionAsync(context, sessionId, deleteSubscriptions)
- .AsTask().GetAwaiter().GetResult();
- }
-
///
/// Closes the specified session.
///
diff --git a/src/Opc.Ua.Server/Server/StandardServer.cs b/src/Opc.Ua.Server/Server/StandardServer.cs
index 8b26a8ba59..c3e258da89 100644
--- a/src/Opc.Ua.Server/Server/StandardServer.cs
+++ b/src/Opc.Ua.Server/Server/StandardServer.cs
@@ -2810,14 +2810,11 @@ namespace Opc.Ua.Server
)]
public ServerStatusDataType GetStatus()
{
- lock (Lock!)
+ if (m_serverInternal == null)
{
- if (m_serverInternal == null)
- {
- throw new ServiceResultException(StatusCodes.BadServerHalted);
- }
- return ServerInternal.Status.Value;
+ throw new ServiceResultException(StatusCodes.BadServerHalted);
}
+ return m_serverInternal.NonThreadSafeStatus.Value;
}
///
@@ -3101,7 +3098,7 @@ namespace Opc.Ua.Server
m_logger.ServerEnterStateState(state);
- ServerInternal.CurrentState = state;
+ m_serverInternal.CurrentState = state;
}
finally
{
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs
index db036ad3c7..231d9a4dfc 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/AddressSpace/DistributedAddressSpaceStartupTaskTests.cs
@@ -97,6 +97,9 @@ namespace Opc.Ua.Server.Tests.Redundancy
server.Setup(s => s.MessageContext).Returns(messageContext);
server.Setup(s => s.NamespaceUris).Returns(messageContext.NamespaceUris);
server.Setup(s => s.NodeManager).Returns(masterNodeManager.Object);
+ server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object));
+ server.Setup(s => s.FindNodeManagers())
+ .Returns([nodeManager.As().Object]);
using var kv = new InMemorySharedKeyValueStore();
var election = new StaticLeaderElection(true);
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs
index 51f54e315d..3860390597 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/Discovery/PeerDiscoveryBuilderExtensionsTests.cs
@@ -135,6 +135,7 @@ namespace Opc.Ua.Server.Tests.Redundancy
var server = new Mock();
server.Setup(s => s.Telemetry).Returns(NUnitTelemetryContext.Create());
+ server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object));
await using (var task = new PeerDiscoveryStartupTask(discovery, provider, options, registry))
{
@@ -158,6 +159,7 @@ namespace Opc.Ua.Server.Tests.Redundancy
var server = new Mock();
server.Setup(s => s.Telemetry).Returns(NUnitTelemetryContext.Create());
+ server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object));
await using (var task = new PeerDiscoveryStartupTask(discovery, provider, options))
{
@@ -180,6 +182,7 @@ namespace Opc.Ua.Server.Tests.Redundancy
var server = new Mock();
server.Setup(s => s.Telemetry).Returns(NUnitTelemetryContext.Create());
+ server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object));
await using (var task = new PeerDiscoveryStartupTask(discovery, provider, options, gossipSink: null))
{
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs
index c930e7aa42..b670d5ee32 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/DiagnosticsServerHarness.cs
@@ -113,6 +113,12 @@ namespace Opc.Ua.Server.Tests.Redundancy
ServerObjectState serverObject = manager.FindPredefinedNode(ObjectIds.Server);
server.Setup(s => s.ServerObject).Returns(serverObject);
server.Setup(s => s.DiagnosticsNodeManager).Returns(manager);
+ server.Setup(s => s.FindNodeManagers()).Returns([manager]);
+ server.Setup(s => s.FindNodeManagers())
+ .Returns([configurationNodeManager.Object]);
+ server.Setup(s => s.FindPredefinedNode(
+ It.IsAny()))
+ .Returns((NodeId nodeId) => manager.FindPredefinedNode(nodeId));
return new DiagnosticsServerHarness(server, manager);
}
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs
index f559027265..0d377c2243 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/LoadDirectionStartupTaskTests.cs
@@ -214,6 +214,7 @@ namespace Opc.Ua.Server.Tests.Redundancy
server.Setup(s => s.Telemetry).Returns(telemetry);
server.Setup(s => s.ServerUris).Returns(serverUris);
server.Setup(s => s.MessageContext).Returns(context);
+ server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object));
return server;
}
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs
index 6459f705e1..7c0a187002 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/PeerDirectionPublishStartupTaskTests.cs
@@ -376,6 +376,7 @@ namespace Opc.Ua.Server.Tests.Redundancy
server.Setup(s => s.Telemetry).Returns(telemetry);
server.Setup(s => s.ServerUris).Returns(serverUris);
server.Setup(s => s.MessageContext).Returns(context);
+ server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object));
return server;
}
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs
index b7ea290f67..ab28063eca 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/RequestServerStateChangeStartupTaskTests.cs
@@ -380,6 +380,11 @@ namespace Opc.Ua.Server.Tests.Redundancy
ServerObjectState serverObject = manager.FindPredefinedNode(ObjectIds.Server);
server.Setup(s => s.ServerObject).Returns(serverObject);
server.Setup(s => s.DiagnosticsNodeManager).Returns(manager);
+ server.Setup(s => s.FindNodeManagers())
+ .Returns([configurationNodeManager.Object]);
+ server.Setup(s => s.FindPredefinedNode(
+ It.IsAny()))
+ .Returns((NodeId nodeId) => manager.FindPredefinedNode(nodeId));
return new LoadedDiagnosticsServer(server, manager, configurationNodeManager);
}
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs
index 49342bbac6..d305ed6ece 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/Redundancy/ServerRedundancyStartupTaskTests.cs
@@ -287,6 +287,7 @@ namespace Opc.Ua.Server.Tests.Redundancy
ServerObjectState serverObject = manager.FindPredefinedNode(ObjectIds.Server);
server.Setup(s => s.ServerObject).Returns(serverObject);
server.Setup(s => s.DiagnosticsNodeManager).Returns(manager);
+ server.Setup(s => s.FindNodeManagers()).Returns([manager]);
return new LoadedDiagnosticsServer(server, manager);
}
diff --git a/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs b/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs
index 08e84748bc..c6380409ba 100644
--- a/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs
+++ b/tests/Opc.Ua.Redundancy.Server.Tests/State/ReplicatedAddressSpaceStartupTaskTests.cs
@@ -88,6 +88,9 @@ namespace Opc.Ua.Redundancy.Server.Tests
server.Setup(s => s.Telemetry).Returns(telemetry);
server.Setup(s => s.MessageContext).Returns(messageContext);
server.Setup(s => s.NodeManager).Returns(masterNodeManager.Object);
+ server.Setup(s => s.DefaultSystemContext).Returns(new ServerSystemContext(server.Object));
+ server.Setup(s => s.FindNodeManagers())
+ .Returns([optedIn.As().Object]);
await using var task = new ReplicatedAddressSpaceStartupTask(
EmptyServices(), new ReplicatedAddressSpaceOptions());
diff --git a/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs b/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs
index c292efeec6..04215ac7cb 100644
--- a/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs
+++ b/tests/Opc.Ua.Server.Tests/Hosting/ServerFluentApiHostingTests.cs
@@ -1031,9 +1031,9 @@ namespace Opc.Ua.Server.Tests.Hosting
{
public int InvocationCount => Volatile.Read(ref m_invocationCount);
- public IServerInternal? ObservedServer { get; private set; }
+ public IServerContext? ObservedServer { get; private set; }
- public ValueTask OnServerStartedAsync(IServerInternal server, CancellationToken cancellationToken = default)
+ public ValueTask OnServerStartedAsync(IServerContext server, CancellationToken cancellationToken = default)
{
Interlocked.Increment(ref m_invocationCount);
ObservedServer = server;
diff --git a/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs b/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs
index f76dfa18d1..2af6d04f9a 100644
--- a/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs
+++ b/tests/Opc.Ua.Server.Tests/ServerInternalDataTests.cs
@@ -49,6 +49,8 @@ namespace Opc.Ua.Server.Tests
private ServiceMessageContext m_messageContext;
private ITelemetryContext m_telemetry;
+ private static readonly string[] s_locales = ["de-DE"];
+
[SetUp]
public void SetUp()
{
@@ -206,12 +208,65 @@ namespace Opc.Ua.Server.Tests
Assert.That(data.NodeManager, Is.SameAs(mockNodeManager.Object));
}
+ [Test]
+ public void FindNodeManagersYieldsAManagerOnBothFacesOnce()
+ {
+ using ServerInternalData data = CreateServerInternalData();
+
+ // A manager that implements INodeManager is its own synchronous adapter, so
+ // the same instance appears on both faces.
+ var manager = new Mock();
+ INodeManager syncFace = manager.As().Object;
+
+ data.SetNodeManager(CreateMasterNodeManager([manager.Object], [syncFace]));
+
+ Assert.That(
+ data.FindNodeManagers().ToList(),
+ Is.EqualTo(new[] { manager.Object }).AsCollection);
+ }
+
+ [Test]
+ public void FindNodeManagersYieldsEveryDistinctManager()
+ {
+ using ServerInternalData data = CreateServerInternalData();
+ var asyncOnly = new Mock();
+ var syncManager = new Mock();
+ IAsyncNodeManager syncAsAsync = syncManager.As().Object;
+
+ data.SetNodeManager(
+ CreateMasterNodeManager([asyncOnly.Object], [syncManager.Object]));
+
+ Assert.That(
+ data.FindNodeManagers().ToList(),
+ Is.EqualTo(new[] { asyncOnly.Object, syncAsAsync }).AsCollection);
+ }
+
+ [Test]
+ public void FindNodeManagersReturnsEmptyWhenNoNodeManagerIsSet()
+ {
+ using ServerInternalData data = CreateServerInternalData();
+
+ Assert.That(data.FindNodeManagers(), Is.Empty);
+ }
+
+ private static IMasterNodeManager CreateMasterNodeManager(
+ IAsyncNodeManager[] asyncNodeManagers,
+ INodeManager[] nodeManagers)
+ {
+ var mock = new Mock();
+ mock.Setup(m => m.DiagnosticsNodeManager).Returns((IDiagnosticsNodeManager)null);
+ mock.Setup(m => m.ConfigurationNodeManager).Returns((IConfigurationNodeManager)null);
+ mock.Setup(m => m.CoreNodeManager).Returns((ICoreNodeManager)null);
+ mock.Setup(m => m.AsyncNodeManagers).Returns(asyncNodeManagers);
+ mock.Setup(m => m.NodeManagers).Returns(nodeManagers);
+ return mock.Object;
+ }
+
[Test]
public void SetMainNodeManagerFactoryStoresFactory()
{
using ServerInternalData data = CreateServerInternalData();
var mockFactory = new Mock();
-
data.SetMainNodeManagerFactory(mockFactory.Object);
Assert.That(data.MainNodeManagerFactory, Is.SameAs(mockFactory.Object));
@@ -321,16 +376,6 @@ namespace Opc.Ua.Server.Tests
Throws.TypeOf());
}
- [Test]
- public void ReadServerDiagnosticsThrowsOnNullRead()
- {
- using ServerInternalData data = CreateServerInternalData();
-
- Assert.That(
- () => data.ReadServerDiagnostics(null!),
- Throws.TypeOf());
- }
-
[Test]
public void DiagnosticsEnabledReturnsFalseWhenNoDiagnosticsNodeManager()
{
@@ -338,6 +383,56 @@ namespace Opc.Ua.Server.Tests
Assert.That(data.DiagnosticsEnabled, Is.False);
}
+ [Test]
+ public void ServerContextDefaultSystemContextIsTheServerSystemContext()
+ {
+ using ServerInternalData data = CreateServerInternalData();
+
+ Assert.That(
+ ((IServerContext)data).DefaultSystemContext,
+ Is.SameAs(data.DefaultSystemContext));
+ }
+
+ [Test]
+ public void CreateSystemContextCarriesTheSessionIdentityAndLocales()
+ {
+ using ServerInternalData data = CreateServerInternalData();
+
+ var sessionId = new NodeId(Guid.NewGuid());
+ var identity = new UserIdentity(new AnonymousIdentityToken());
+
+ var session = new Mock();
+ session.Setup(s => s.Id).Returns(sessionId);
+ session.Setup(s => s.Identity).Returns(identity);
+ session.Setup(s => s.PreferredLocales).Returns(s_locales);
+
+ ServerSystemContext created = data.CreateSystemContext(session.Object);
+
+ Assert.That(created, Is.Not.SameAs(data.DefaultSystemContext));
+ Assert.That(created.PreferredLocales.ToArray(), Is.EqualTo(s_locales));
+ Assert.That(created.NamespaceUris, Is.SameAs(data.NamespaceUris));
+ }
+
+ [Test]
+ public void CreateSystemContextThrowsOnNullSession()
+ {
+ using ServerInternalData data = CreateServerInternalData();
+
+ Assert.That(
+ () => data.CreateSystemContext(null!),
+ Throws.TypeOf());
+ }
+
+ [Test]
+ public void FindPredefinedNodeReturnsNullBeforeTheDiagnosticsNodeManagerExists()
+ {
+ using ServerInternalData data = CreateServerInternalData();
+
+ // The datastore is published before the node managers are bound, so callers
+ // must tolerate an address space that is not there yet.
+ Assert.That(data.FindPredefinedNode(ObjectIds.Server), Is.Null);
+ }
+
[Test]
public void TelemetryReturnsMessageContextTelemetry()
{
diff --git a/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs b/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs
index 815b886f08..878872e08f 100644
--- a/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs
+++ b/tests/Opc.Ua.Server.Tests/SessionSecurityTests.cs
@@ -73,7 +73,6 @@ namespace Opc.Ua.Server.Tests
m_server.Setup(s => s.MessageContext).Returns(
ServiceMessageContext.CreateEmpty(m_telemetry));
var serverDiagnostics = new ServerDiagnosticsSummaryDataType();
- m_server.Setup(s => s.ServerDiagnostics).Returns(serverDiagnostics);
m_server
.Setup(s => s.UpdateServerDiagnostics(
It.IsAny>()))
diff --git a/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs b/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs
index 8134839e90..e27f0c3097 100644
--- a/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs
+++ b/tests/Opc.Ua.Server.Tests/StandardServerCoverageTests.cs
@@ -165,8 +165,10 @@ namespace Opc.Ua.Server.Tests
using TestableStandardServer server = CreateServer();
#pragma warning disable CS0618 // GetStatus is obsolete but still exercised for coverage.
- Assert.That(() => server.GetStatus(), Throws.Exception);
+ ServiceResultException ex = Assert.Throws(
+ () => server.GetStatus());
#pragma warning restore CS0618
+ Assert.That(ex.StatusCode, Is.EqualTo((uint)StatusCodes.BadServerHalted));
}
[Test]
diff --git a/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs b/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs
index 51ad54b75a..e7ef179936 100644
--- a/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs
+++ b/tests/Opc.Ua.Server.Tests/SubscriptionTests.cs
@@ -68,7 +68,6 @@ namespace Opc.Ua.Server.Tests
m_serverMock.Setup(s => s.NodeManager).Returns(m_nodeManagerMock.Object);
m_serverMock.Setup(s => s.MonitoredItemQueueFactory).Returns(m_queueFactoryMock.Object);
var serverDiagnostics = new ServerDiagnosticsSummaryDataType();
- m_serverMock.Setup(s => s.ServerDiagnostics).Returns(serverDiagnostics);
m_serverMock
.Setup(s => s.UpdateServerDiagnostics(
It.IsAny>()))
diff --git a/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md b/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md
index 99862abafc..415301d53d 100644
--- a/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md
+++ b/tools/Opc.Ua.MigrationAnalyzer/AnalyzerReleases.Unshipped.md
@@ -25,6 +25,6 @@ UA0020 | Migration | Warning | Replace `EncodeableFactory.GlobalFactory` / `En
UA0021 | Migration | Info | Replace `CertificateValidator` / `CertificateValidationEventArgs` with the 1.6 `ICertificateManager` / `ICertificateValidatorEx` / `CertificateValidationResult` pipeline. See docs/migrate/2.0.x/certificates.md.
UA0022 | Migration | Warning | Replace `ApplicationConfiguration.CertificateValidator` / `ServerBase.CertificateValidator` property access with `.CertificateManager`.
UA0023 | Migration | Warning | Replace the legacy 1.04 PubSub top-level types (`UaPubSubApplication`, `IUaPubSubConnection`, `UaPubSubConnection`, `IUaPublisher`, `UaPublisher`, `IUaPubSubDataStore`, `UaPubSubDataStore`, `UaPubSubConfigurator`) with the new `IPubSubApplication` / `PubSubApplicationBuilder` surface (or `AddPubSub()` / `AddUdpTransport()` / `AddMqttTransport()` on `IOpcUaBuilder`). See docs/migrate/2.0.x/pubsub.md.
-UA0024 | Migration | Warning | Replace `IServerInternal`/`ISession`/`ISubscription` `DiagnosticsLock`/`DiagnosticsWriteLock` access with `UpdateServerDiagnostics(...)` / `UpdateDiagnostics(...)` (and `ReadServerDiagnostics` / `ReadDiagnostics` for projections).
+UA0024 | Migration | Warning | Replace `IServerInternal`/`ISession`/`ISubscription` `DiagnosticsLock`/`DiagnosticsWriteLock` access with `UpdateServerDiagnostics(...)` / `UpdateDiagnostics(...)` (and `ReadDiagnostics` for projections).
UA0025 | Migration | Warning | Replace `ILocalNode.DataLock` / `Node.DataLock` access with a lock you own; the node guards its own state.
UA0026 | Migration | Warning | Replace `BaseVariableValue.Lock` access with a lock you own and pass to the `BaseVariableValue` constructor.
diff --git a/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs b/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs
index 15b1badfaa..48fa5db0d5 100644
--- a/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs
+++ b/tools/Opc.Ua.MigrationAnalyzer/Diagnostics/DiagnosticDescriptors.cs
@@ -202,7 +202,7 @@ namespace Opc.Ua.MigrationAnalyzer.Diagnostics
"Diagnostics locks are no longer exposed",
"'{0}.{1}' was removed in 2.0 — apply the change through '{0}.{2}' instead of taking the lock yourself",
DiagnosticSeverity.Warning,
- "IServerInternal, ISession and ISubscription no longer expose DiagnosticsLock / DiagnosticsWriteLock. A caller could not see what else took those locks, in what order, or for how long, and holding one across a call back into the stack could deadlock. Each owner now applies the mutation itself through UpdateDiagnostics / UpdateServerDiagnostics, with ReadDiagnostics / ReadServerDiagnostics for projections. Do not let the diagnostics object escape the callback: once it returns the lock is released. This rule reports rather than fixes, because turning a lock statement body into a lambda depends on what the body captures and returns.");
+ "IServerInternal, ISession and ISubscription no longer expose DiagnosticsLock / DiagnosticsWriteLock. A caller could not see what else took those locks, in what order, or for how long, and holding one across a call back into the stack could deadlock. Each owner now applies the mutation itself through UpdateDiagnostics / UpdateServerDiagnostics, and ISession and ISubscription add ReadDiagnostics for projections. Do not let the diagnostics object escape the callback: once it returns the lock is released. This rule reports rather than fixes, because turning a lock statement body into a lambda depends on what the body captures and returns.");
public static readonly DiagnosticDescriptor UA0025_RemovedNodeDataLock = Create(
DiagnosticIds.UA0025,