diff --git a/Libraries/Opc.Ua.Server/Session/ISessionManager.cs b/Libraries/Opc.Ua.Server/Session/ISessionManager.cs
index 1eb759c75..29ee3ac84 100644
--- a/Libraries/Opc.Ua.Server/Session/ISessionManager.cs
+++ b/Libraries/Opc.Ua.Server/Session/ISessionManager.cs
@@ -58,6 +58,11 @@ public interface ISessionManager : IDisposable
///
event SessionEventHandler SessionClosing;
+ ///
+ /// Raised after diagnostics of an existing session were changed.
+ ///
+ event SessionEventHandler SessionDiagnosticsChanged;
+
///
/// Raised to signal a channel that the session is still alive.
///
@@ -141,6 +146,11 @@ ValueTask CreateSessionAsync(
/// and that the sequence number is not out of order (update requests only).
///
OperationContext ValidateRequest(RequestHeader requestHeader, SecureChannelContext secureChannelContext, RequestType requestType);
+
+ ///
+ /// Triggers the event so subscribers can react.
+ ///
+ void RaiseSessionDiagnosticsChangedEvent(ISession session);
}
///
@@ -194,6 +204,11 @@ public enum SessionEventReason
///
Activated,
+ ///
+ /// The diagnostics of an existing session were changed.
+ ///
+ DiagnosticsChanged,
+
///
/// A session is about to be closed.
///
diff --git a/Libraries/Opc.Ua.Server/Session/Session.cs b/Libraries/Opc.Ua.Server/Session/Session.cs
index e6d0f6a94..fb348f74e 100644
--- a/Libraries/Opc.Ua.Server/Session/Session.cs
+++ b/Libraries/Opc.Ua.Server/Session/Session.cs
@@ -350,7 +350,6 @@ public virtual void ValidateRequest(RequestHeader requestHeader, SecureChannelCo
lock (m_lock)
{
-
if (secureChannelContext == null || !IsSecureChannelValid(secureChannelContext.SecureChannelId))
{
UpdateDiagnosticCounters(requestType, true, true);
@@ -1141,6 +1140,8 @@ private void UpdateDiagnosticCounters(
bool error,
bool authorizationError)
{
+ ServiceCounterDataType counter = null;
+
lock (DiagnosticsLock)
{
if (!error)
@@ -1160,8 +1161,6 @@ private void UpdateDiagnosticCounters(
}
}
- ServiceCounterDataType counter = null;
-
switch (requestType)
{
case RequestType.Read:
@@ -1271,6 +1270,11 @@ private void UpdateDiagnosticCounters(
}
}
}
+
+ if (counter != null)
+ {
+ m_server.SessionManager.RaiseSessionDiagnosticsChangedEvent(this);
+ }
}
private readonly Lock m_lock = new();
diff --git a/Libraries/Opc.Ua.Server/Session/SessionManager.cs b/Libraries/Opc.Ua.Server/Session/SessionManager.cs
index 25b1898e6..a087b3bf8 100644
--- a/Libraries/Opc.Ua.Server/Session/SessionManager.cs
+++ b/Libraries/Opc.Ua.Server/Session/SessionManager.cs
@@ -571,6 +571,17 @@ protected virtual ISession CreateSession(
m_maxHistoryContinuationPoints);
}
+ ///
+ public virtual void RaiseSessionDiagnosticsChangedEvent(ISession session)
+ {
+ if (session == null)
+ {
+ throw new ArgumentNullException(nameof(session));
+ }
+
+ RaiseSessionEvent(session, SessionEventReason.DiagnosticsChanged);
+ }
+
///
/// Raises an event related to a session.
///
@@ -592,6 +603,9 @@ protected virtual void RaiseSessionEvent(ISession session, SessionEventReason re
case SessionEventReason.Closing:
handler = m_SessionClosing;
break;
+ case SessionEventReason.DiagnosticsChanged:
+ handler = m_SessionDiagnosticsChanged;
+ break;
case SessionEventReason.ChannelKeepAlive:
handler = m_SessionChannelKeepAlive;
break;
@@ -688,6 +702,7 @@ await m_server.CloseSessionAsync(null, session.Id, false)
private event SessionEventHandler m_SessionCreated;
private event SessionEventHandler m_SessionActivated;
private event SessionEventHandler m_SessionClosing;
+ private event SessionEventHandler m_SessionDiagnosticsChanged;
private event SessionEventHandler m_SessionChannelKeepAlive;
private event ImpersonateEventHandler m_ImpersonateUser;
private event EventHandler m_ValidateSessionLessRequest;
@@ -730,6 +745,25 @@ public event SessionEventHandler SessionActivated
}
}
+ ///
+ public event SessionEventHandler SessionDiagnosticsChanged
+ {
+ add
+ {
+ lock (m_eventLock)
+ {
+ m_SessionDiagnosticsChanged += value;
+ }
+ }
+ remove
+ {
+ lock (m_eventLock)
+ {
+ m_SessionDiagnosticsChanged -= value;
+ }
+ }
+ }
+
///
public event SessionEventHandler SessionClosing
{
diff --git a/Tests/Opc.Ua.Server.Tests/SessionTests.cs b/Tests/Opc.Ua.Server.Tests/SessionTests.cs
new file mode 100644
index 000000000..8812489bd
--- /dev/null
+++ b/Tests/Opc.Ua.Server.Tests/SessionTests.cs
@@ -0,0 +1,95 @@
+using System.Threading.Tasks;
+using NUnit.Framework;
+
+using Assert = NUnit.Framework.Legacy.ClassicAssert;
+
+namespace Opc.Ua.Server.Tests
+{
+ [TestFixture]
+ [Category("Event")]
+ public class SessionTests
+ {
+ [Test]
+ public async Task UpdateDiagnosticCounters_RaisesEvent_WhenPerRequestCounterChanged()
+ {
+ var fixture = new ServerFixture();
+ await fixture.StartAsync().ConfigureAwait(false);
+
+ try
+ {
+ StandardServer server = fixture.Server;
+
+ (RequestHeader requestHeader, SecureChannelContext secureChannelContext) =
+ await ServerFixtureUtils.CreateAndActivateSessionAsync(server, "UpdateDiagnosticCountersTest").ConfigureAwait(false);
+
+ ISession session = server.CurrentInstance.SessionManager.GetSession(requestHeader.AuthenticationToken);
+ Assert.NotNull(session, "Session should exist after Create/Activate.");
+
+ bool eventRaised = false;
+
+ server.CurrentInstance.SessionManager.SessionDiagnosticsChanged += (s, reason)
+ => eventRaised = true;
+
+ uint before = session.SessionDiagnostics.ReadCount.TotalCount;
+
+ // Call ValidateRequest for a request type that maps to a counter (Read).
+ session.ValidateRequest(requestHeader, secureChannelContext, RequestType.Read);
+
+ Assert.IsTrue(eventRaised, "SessionDiagnosticsChanged event should be raised when a per-request counter changes.");
+ Assert.Greater(session.SessionDiagnostics.ReadCount.TotalCount, before, "ReadCount.TotalCount should have incremented.");
+ }
+ finally
+ {
+ await fixture.StopAsync().ConfigureAwait(false);
+ }
+ }
+
+ [Test]
+ [Category("Event")]
+ [TestCase(RequestType.Unknown)]
+ [TestCase(RequestType.FindServers)]
+ [TestCase(RequestType.GetEndpoints)]
+ [TestCase(RequestType.CreateSession)]
+ [TestCase(RequestType.ActivateSession)]
+ [TestCase(RequestType.CloseSession)]
+ [TestCase(RequestType.Cancel)]
+ public async Task UpdateDiagnosticCounters_DoesNotRaiseEvent_ForIgnoredRequestTypes(RequestType requestType)
+ {
+ var fixture = new ServerFixture();
+ await fixture.StartAsync().ConfigureAwait(false);
+
+ try
+ {
+ StandardServer server = fixture.Server;
+
+ (RequestHeader requestHeader, SecureChannelContext secureChannelContext) =
+ await ServerFixtureUtils.CreateAndActivateSessionAsync(server, "UpdateDiagnosticCountersIgnoredTest").ConfigureAwait(false);
+
+ ISession session = server.CurrentInstance.SessionManager.GetSession(requestHeader.AuthenticationToken);
+ Assert.NotNull(session, "Session should exist after Create/Activate.");
+
+ bool eventRaised = false;
+
+ server.CurrentInstance.SessionManager.SessionDiagnosticsChanged += (s, reason)
+ => eventRaised = true;
+
+ // Capture total requests before; UpdateDiagnosticCounters always increments TotalRequestCount.
+ uint totalBefore = session.SessionDiagnostics.TotalRequestCount.TotalCount;
+
+ // Call ValidateRequest with one of the ignored request types.
+ session.ValidateRequest(requestHeader, secureChannelContext, requestType);
+
+ Assert.AreEqual(
+ totalBefore + 1,
+ session.SessionDiagnostics.TotalRequestCount.TotalCount,
+ "TotalRequestCount should increment for all request types.");
+
+ Assert.IsFalse(eventRaised, $"SessionDiagnosticsChanged event must NOT be raised for request type {requestType}.");
+ }
+ finally
+ {
+ await fixture.StopAsync().ConfigureAwait(false);
+ }
+ }
+ }
+}