From 02fa2569394d58d1b5d4591db87cd64eabd40eb7 Mon Sep 17 00:00:00 2001
From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com>
Date: Wed, 7 Jan 2026 11:58:36 +0100
Subject: [PATCH 1/6] Add caching for message counts and update observer init
Introduce 500ms caching for ActiveMessagesCount and RelevantMessagesCount in AxoMessageProvider to reduce flickering and improve performance. Ensure thread safety with locks and add InvalidateMessageCountCache for manual cache invalidation. Update AxoObjectSpotView.razor to use InitializeUpdate instead of InitializeLightUpdate.
---
.../AxoObject/AxoObjectSpotView.razor | 2 +-
.../AxoMessenger/Static/AxoMessageProvider.cs | 106 ++++++++++++++----
2 files changed, 85 insertions(+), 23 deletions(-)
diff --git a/src/core/src/AXOpen.Core.Blazor/AxoObject/AxoObjectSpotView.razor b/src/core/src/AXOpen.Core.Blazor/AxoObject/AxoObjectSpotView.razor
index 63f365959..755d1b634 100644
--- a/src/core/src/AXOpen.Core.Blazor/AxoObject/AxoObjectSpotView.razor
+++ b/src/core/src/AXOpen.Core.Blazor/AxoObject/AxoObjectSpotView.razor
@@ -92,7 +92,7 @@
///
protected override async Task OnInitializedAsync()
{
- if (observer != null) await observer.InitializeLightUpdate(this.StartPolling);
+ if (observer != null) await observer.InitializeUpdate(this.StartPolling);
await base.OnInitializedAsync();
}
}
diff --git a/src/core/src/AXOpen.Core/AxoMessenger/Static/AxoMessageProvider.cs b/src/core/src/AXOpen.Core/AxoMessenger/Static/AxoMessageProvider.cs
index a050ae1d5..e6f77c2d2 100644
--- a/src/core/src/AXOpen.Core/AxoMessenger/Static/AxoMessageProvider.cs
+++ b/src/core/src/AXOpen.Core/AxoMessenger/Static/AxoMessageProvider.cs
@@ -23,59 +23,102 @@ private AxoMessageProvider(IEnumerable observedObjects)
public IEnumerable ObservedObjects { get; }
+ private int? _cachedActiveMessagesCount;
+ private DateTime _lastActiveMessagesCountUpdate = DateTime.MinValue;
+ private readonly TimeSpan _activeMessagesCountCacheDuration = TimeSpan.FromMilliseconds(500);
+ private readonly object _activeMessagesCountLock = new object();
+
///
/// Gets the number of active messages.
///
///
/// This property counts the number of messages that are currently active.
/// An active message is defined as a message belonging to a Messenger that has a state other than Idle or NotActiveWatingAckn.
+ /// The value is cached for a short period to prevent flickering due to asynchronous PLC communication.
///
public int? ActiveMessagesCount
{
get
{
- try
- {
- return ObservedObjects
- .OfType()
- .Select(p => Convert.ToInt32(p.MsgCnt.LastValue)) // Convert to appropriate numeric type
- .Sum();
- }
- catch (Exception e)
+ lock (_activeMessagesCountLock)
{
- Console.WriteLine(e);
+ var now = DateTime.UtcNow;
+
+ // Return cached value if still valid
+ if (_cachedActiveMessagesCount.HasValue &&
+ (now - _lastActiveMessagesCountUpdate) < _activeMessagesCountCacheDuration)
+ {
+ return _cachedActiveMessagesCount;
+ }
+
+ try
+ {
+ var count = ObservedObjects
+ .OfType()
+ .Select(p => Convert.ToInt32(p.MsgCnt.LastValue))
+ .Sum();
+
+ _cachedActiveMessagesCount = count;
+ _lastActiveMessagesCountUpdate = now;
+
+ return count;
+ }
+ catch (Exception e)
+ {
+ return -1;
+ }
}
-
- return 0;
}
}
+ private int? _cachedRelevantMessagesCount;
+ private DateTime _lastRelevantMessagesCountUpdate = DateTime.MinValue;
+ private readonly TimeSpan _relevantMessagesCountCacheDuration = TimeSpan.FromMilliseconds(500);
+ private readonly object _relevantMessagesCountLock = new object();
+
///
/// Gets the count of relevant messages based on the state of Messengers.
///
///
/// The RelevantMessagesCount property will return the number of messengers that have a state greater than eAxoMessengerState.Idle.
/// Messengers is a collection of objects that represents messengers.
+ /// The value is cached for a short period to prevent flickering due to asynchronous PLC communication.
///
/// An integer that represents the count of relevant messages.
public int? RelevantMessagesCount
{
get
{
- try
- {
- return ObservedObjects
- .OfType()
- .Select(p => Convert.ToInt32(p.MsgCnt.LastValue)) // Convert to appropriate numeric type
- .Sum();
- }
- catch (Exception e)
+ lock (_relevantMessagesCountLock)
{
- Console.WriteLine(e);
- }
+ var now = DateTime.UtcNow;
+
+ // Return cached value if still valid
+ if (_cachedRelevantMessagesCount.HasValue &&
+ (now - _lastRelevantMessagesCountUpdate) < _relevantMessagesCountCacheDuration)
+ {
+ return _cachedRelevantMessagesCount;
+ }
- return 0;
+ try
+ {
+ var count = ObservedObjects
+ .OfType()
+ .Select(p => Convert.ToInt32(p.MsgCnt.LastValue))
+ .Sum();
+
+ _cachedRelevantMessagesCount = count;
+ _lastRelevantMessagesCountUpdate = now;
+
+ return count;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ return 0;
+ }
+ }
}
}
@@ -190,5 +233,24 @@ public async Task ReadMessageStateAsync()
await con.ReadBatchAsync(r)!;
}
}
+
+ ///
+ /// Invalidates the cached message counts, forcing them to be recalculated on next access.
+ /// Use this method if you need to ensure fresh values after a batch read operation.
+ ///
+ public void InvalidateMessageCountCache()
+ {
+ lock (_activeMessagesCountLock)
+ {
+ _cachedActiveMessagesCount = null;
+ _lastActiveMessagesCountUpdate = DateTime.MinValue;
+ }
+
+ lock (_relevantMessagesCountLock)
+ {
+ _cachedRelevantMessagesCount = null;
+ _lastRelevantMessagesCountUpdate = DateTime.MinValue;
+ }
+ }
}
}
From 47bb376fdee5eec5d397a3a58f6f092953895b4d Mon Sep 17 00:00:00 2001
From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com>
Date: Fri, 9 Jan 2026 08:43:07 +0100
Subject: [PATCH 2/6] wip
---
.../Data/IBrowsableDataObject.cs | 5 ++++-
.../Data/Query/PredicateContainer.cs | 10 ++++++++++
.../AXOpen.Base.Abstractions/Data/RepositoryBase.cs | 4 +++-
src/core/ctrl/src/AxoObject/AxoObject.st | 10 +++++++++-
.../DataFragmentExchange/AxoDataFragmentExchange.cs | 2 +-
.../DataPersistentExchange/PersistentRecord.cs | 7 +++++++
src/data/src/AXOpen.Data/Entity/AxoDataEntity.cs | 1 +
src/data/src/AXOpen.Data/Entity/Pocos/AxoDataEntity.cs | 3 +++
.../src/AXOpen.Data/Entity/Pocos/IAxoDataEntity.cs | 2 ++
.../repositories/MongoDb/Mongo/MongoDbRepository.cs | 6 ++++++
.../MongoDb/Mongo/MongoDbRepositorySettings.cs | 10 +++++++---
src/security/src/AXOpen.Security/Entities/Group.cs | 8 ++++++++
src/security/src/AXOpen.Security/Entities/User.cs | 7 +++++++
13 files changed, 68 insertions(+), 7 deletions(-)
diff --git a/src/base/src/AXOpen.Base.Abstractions/Data/IBrowsableDataObject.cs b/src/base/src/AXOpen.Base.Abstractions/Data/IBrowsableDataObject.cs
index b74ab9f8e..9fd8dfbf4 100644
--- a/src/base/src/AXOpen.Base.Abstractions/Data/IBrowsableDataObject.cs
+++ b/src/base/src/AXOpen.Base.Abstractions/Data/IBrowsableDataObject.cs
@@ -6,6 +6,9 @@ public interface IBrowsableDataObject
{
dynamic RecordId { get; set; }
- string _EntityId { get; set; }
+ string _EntityId { get; set; }
+
+ DateTime? ModifiedAt { get; set; }
+ DateTime? CreatedAt { get; set; }
}
}
\ No newline at end of file
diff --git a/src/base/src/AXOpen.Base.Abstractions/Data/Query/PredicateContainer.cs b/src/base/src/AXOpen.Base.Abstractions/Data/Query/PredicateContainer.cs
index 506d92d4a..e9e8cf0dc 100644
--- a/src/base/src/AXOpen.Base.Abstractions/Data/Query/PredicateContainer.cs
+++ b/src/base/src/AXOpen.Base.Abstractions/Data/Query/PredicateContainer.cs
@@ -125,6 +125,16 @@ public void AddPredicates(Expression> predicate)
this._predicates[typeof(T)].Add(predicate);
}
+ public void AddPredicates(Type type, Expression> expression)
+ {
+ if (!this._predicates.ContainsKey(type))
+ {
+ this._predicates[type] = new List();
+ }
+
+ this._predicates[type].Add(expression);
+ }
+
public void AddPredicates(Type type, LambdaExpression predicate)
{
if (!this._predicates.ContainsKey(type))
diff --git a/src/base/src/AXOpen.Base.Abstractions/Data/RepositoryBase.cs b/src/base/src/AXOpen.Base.Abstractions/Data/RepositoryBase.cs
index 9db455f5a..86d19a7d4 100644
--- a/src/base/src/AXOpen.Base.Abstractions/Data/RepositoryBase.cs
+++ b/src/base/src/AXOpen.Base.Abstractions/Data/RepositoryBase.cs
@@ -351,6 +351,7 @@ public void Create(string identifier, T data)
}
try
{
+ data.ModifiedAt = DateTime.UtcNow;
CreateNvi(identifier, data);
}
catch (Exception e)
@@ -409,12 +410,13 @@ public void Update(string identifier, T data)
}
try
{
+ data.ModifiedAt = DateTime.UtcNow;
UpdateNvi(identifier, data);
}
catch (Exception e)
{
OnUpdateFailed?.Invoke(identifier, data, e);
- throw e;
+ throw;
}
OnUpdateDone?.Invoke(identifier, data);
}
diff --git a/src/core/ctrl/src/AxoObject/AxoObject.st b/src/core/ctrl/src/AxoObject/AxoObject.st
index aac296d8d..9948b5087 100644
--- a/src/core/ctrl/src/AxoObject/AxoObject.st
+++ b/src/core/ctrl/src/AxoObject/AxoObject.st
@@ -95,6 +95,9 @@ NAMESPACE AXOpen.Core
NULL_CONTEXT_OBJ : IAxoObject;
END_VAR
+ // We always reset message count upon initialization.
+ THIS.MsgCnt := LINT#0;
+
IF _isInitialized THEN
RETURN;
END_IF;
@@ -117,7 +120,7 @@ NAMESPACE AXOpen.Core
_errorState := UINT#40;
RETURN;
END_IF;
-
+
_context := inParent.GetContext();
_parent := inParent;
_isInitialized := TRUE;
@@ -141,6 +144,9 @@ NAMESPACE AXOpen.Core
inContext : IAxoContext;
END_VAR
+
+ // We always reset message count upon initialization.
+ THIS.MsgCnt := LINT#0;
IF _isInitialized THEN
RETURN;
END_IF;
@@ -151,6 +157,8 @@ NAMESPACE AXOpen.Core
RETURN;
END_IF;
+
+
_context := inContext;
_isInitialized := TRUE;
diff --git a/src/data/src/AXOpen.Data/DataFragmentExchange/AxoDataFragmentExchange.cs b/src/data/src/AXOpen.Data/DataFragmentExchange/AxoDataFragmentExchange.cs
index 083c609a3..d646f03ef 100644
--- a/src/data/src/AXOpen.Data/DataFragmentExchange/AxoDataFragmentExchange.cs
+++ b/src/data/src/AXOpen.Data/DataFragmentExchange/AxoDataFragmentExchange.cs
@@ -500,7 +500,7 @@ public IEnumerable GetRecords(string identifier, int limit
{
return ((dynamic)Repository)?.GetRecords(identifier, limit, skip, searchMode, sortExpression, sortAscending);
}
-
+
public IEnumerable GetRecords(PredicateContainer predicates,
int limit, int skip)
{
diff --git a/src/data/src/AXOpen.Data/DataPersistentExchange/PersistentRecord.cs b/src/data/src/AXOpen.Data/DataPersistentExchange/PersistentRecord.cs
index b663cd7ee..1897a432c 100644
--- a/src/data/src/AXOpen.Data/DataPersistentExchange/PersistentRecord.cs
+++ b/src/data/src/AXOpen.Data/DataPersistentExchange/PersistentRecord.cs
@@ -1,4 +1,5 @@
using AXOpen.Base.Data;
+using Microsoft.AspNetCore.Http.HttpResults;
namespace AXOpen.Data
{
@@ -11,6 +12,12 @@ public class PersistentRecord : IBrowsableDataObject
public DateTime _Created { set; get; }
public DateTime _Modified { set; get; }
+ // Added due to IBrowsableDataObject interface and compatibility with Prometheus.
+ public DateTime? ModifiedAt { get { return _Modified; } set { _Modified = value.Value; } }
+
+ // Added due to IBrowsableDataObject interface and compatibility with Prometheus.
+ public DateTime? CreatedAt { get { return _Created; } set { _Created = value.Value; } }
+
public List Tags = new();
}
}
\ No newline at end of file
diff --git a/src/data/src/AXOpen.Data/Entity/AxoDataEntity.cs b/src/data/src/AXOpen.Data/Entity/AxoDataEntity.cs
index 7c5c206e6..7c8460e7b 100644
--- a/src/data/src/AXOpen.Data/Entity/AxoDataEntity.cs
+++ b/src/data/src/AXOpen.Data/Entity/AxoDataEntity.cs
@@ -14,6 +14,7 @@ partial void PostConstruct(ITwinObject parent, string readableTail, string symbo
ChangeTracker = new ValueChangeTracker(this);
}
+
public List Changes { get; set; }
public string Hash { get; set; }
diff --git a/src/data/src/AXOpen.Data/Entity/Pocos/AxoDataEntity.cs b/src/data/src/AXOpen.Data/Entity/Pocos/AxoDataEntity.cs
index 2e676e7a7..a5046a1e5 100644
--- a/src/data/src/AXOpen.Data/Entity/Pocos/AxoDataEntity.cs
+++ b/src/data/src/AXOpen.Data/Entity/Pocos/AxoDataEntity.cs
@@ -22,5 +22,8 @@ public List Changes
}
public string Hash { get; set; }
+
+ public DateTime? ModifiedAt { get; set; }
+ public DateTime? CreatedAt { get; set; }
}
}
\ No newline at end of file
diff --git a/src/data/src/AXOpen.Data/Entity/Pocos/IAxoDataEntity.cs b/src/data/src/AXOpen.Data/Entity/Pocos/IAxoDataEntity.cs
index 22d13c042..5469c0567 100644
--- a/src/data/src/AXOpen.Data/Entity/Pocos/IAxoDataEntity.cs
+++ b/src/data/src/AXOpen.Data/Entity/Pocos/IAxoDataEntity.cs
@@ -10,6 +10,8 @@ public partial interface IAxoDataEntity : IBrowsableDataObject
public string _EntityId { get; set; }
List Changes { get; set; }
+
+
string Hash { get; set; }
}
}
diff --git a/src/data/src/repositories/MongoDb/Mongo/MongoDbRepository.cs b/src/data/src/repositories/MongoDb/Mongo/MongoDbRepository.cs
index e2a302ab3..c246779f0 100644
--- a/src/data/src/repositories/MongoDb/Mongo/MongoDbRepository.cs
+++ b/src/data/src/repositories/MongoDb/Mongo/MongoDbRepository.cs
@@ -39,6 +39,11 @@ public class MongoDbRepository : RepositoryBase
public override long LastFragmentQueryCount { get; protected set; }
+ ///
+ /// Gets repository settings.
+ ///
+ public MongoDbRepositorySettings Settings { get; private set; }
+
///
/// Creates new instance of .
///
@@ -47,6 +52,7 @@ public MongoDbRepository(MongoDbRepositorySettings parameters)
{
location = parameters.GetConnectionInfo();
this.collection = parameters.Collection;
+ Settings = parameters;
}
private bool RecordExists(string identifier)
diff --git a/src/data/src/repositories/MongoDb/Mongo/MongoDbRepositorySettings.cs b/src/data/src/repositories/MongoDb/Mongo/MongoDbRepositorySettings.cs
index 1288cc2fb..0a3792702 100644
--- a/src/data/src/repositories/MongoDb/Mongo/MongoDbRepositorySettings.cs
+++ b/src/data/src/repositories/MongoDb/Mongo/MongoDbRepositorySettings.cs
@@ -21,7 +21,11 @@ namespace AXOpen.Data.MongoDb
public class MongoDbRepositorySettings : RepositorySettings where T : IBrowsableDataObject
{
private string _databaseName;
- private string _collectionName;
+
+ ///
+ /// Gets collection name.
+ ///
+ public string CollectionName { get; private set; }
///
/// Creates new instance of for a with NON-SECURED access.
@@ -144,7 +148,7 @@ private IMongoDatabase GetDatabase(string databaseName)
}
private IMongoCollection GetCollection(string collectionName)
{
- _collectionName = collectionName;
+ CollectionName = collectionName;
var existingClient = Collections.Where(p => p.Key == collectionName).Select(p => p.Value);
if (existingClient.Count() >= 1)
{
@@ -258,7 +262,7 @@ private static void SetupSerialisationAndMapping(Expression> idE
public string GetConnectionInfo()
{
- return $"{this.Client.Settings.Server.Host}:{this.Client.Settings.Server.Port} {this._databaseName}.{this._collectionName}";
+ return $"{this.Client.Settings.Server.Host}:{this.Client.Settings.Server.Port} {this._databaseName}.{this.CollectionName}";
}
public void WaitForMongoServerAvailability()
diff --git a/src/security/src/AXOpen.Security/Entities/Group.cs b/src/security/src/AXOpen.Security/Entities/Group.cs
index 70e9c6585..7231ef770 100644
--- a/src/security/src/AXOpen.Security/Entities/Group.cs
+++ b/src/security/src/AXOpen.Security/Entities/Group.cs
@@ -12,6 +12,14 @@ public class Group : IBrowsableDataObject
public string RolesHash { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
+
+ // Added due to IBrowsableDataObject interface and compatibility with Prometheus.
+ public DateTime? ModifiedAt { get { return Modified; } set { Modified = value.Value; } }
+
+ // Added due to IBrowsableDataObject interface and compatibility with Prometheus.
+ public DateTime? CreatedAt { get { return Created; } set { Created = value.Value; } }
+
+
public List Changes = new List();
public Group(string name)
diff --git a/src/security/src/AXOpen.Security/Entities/User.cs b/src/security/src/AXOpen.Security/Entities/User.cs
index f5124ad45..4fd7ffad7 100644
--- a/src/security/src/AXOpen.Security/Entities/User.cs
+++ b/src/security/src/AXOpen.Security/Entities/User.cs
@@ -12,6 +12,13 @@ public class User : IdentityUser, IBrowsableDataObject
public string _EntityId { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
+
+ // Added due to IBrowsableDataObject interface and compatibility with Prometheus.
+ public DateTime? ModifiedAt { get { return Modified; } set { Modified = value.Value; } }
+
+ // Added due to IBrowsableDataObject interface and compatibility with Prometheus.
+ public DateTime? CreatedAt { get { return Created; } set { Created = value.Value; } }
+
public bool EnableAutoLogOut { get; set; }
public uint AutoLogOutTimeOutMinutes { get; set; }
public string? ExternalAuthId { get; set; }
From 9bc9196894d5a482c6557a76de85c42c6fe12099 Mon Sep 17 00:00:00 2001
From: Peter Kurhajec <61538034+PTKu@users.noreply.github.com>
Date: Tue, 13 Jan 2026 11:41:47 +0100
Subject: [PATCH 3/6] Fix null reference issues in logging methods and clean up
MongoDB project references
---
src/base/src/AXOpen.Logging/SerilogLogger.cs | 24 +++++++++----------
.../MongoDb/AXOpen.Data.MongoDb.csproj | 5 ++--
2 files changed, 14 insertions(+), 15 deletions(-)
diff --git a/src/base/src/AXOpen.Logging/SerilogLogger.cs b/src/base/src/AXOpen.Logging/SerilogLogger.cs
index adb250d80..b20c4ad34 100644
--- a/src/base/src/AXOpen.Logging/SerilogLogger.cs
+++ b/src/base/src/AXOpen.Logging/SerilogLogger.cs
@@ -30,83 +30,83 @@ public SerilogLogger(Serilog.ILogger logger)
public void Debug(string message, IIdentity identity)
{
- Log.Debug("{message} {identity}",message, new { UserName = identity.Name });
+ Log.Debug("{message} {identity}",message, new { UserName = identity?.Name });
}
public void Debug(string message, ITwinElement sender, IIdentity identity, object details)
{
Log.Debug