diff --git a/AyCode.Models.Server/AyCode.Models.Server.csproj b/AyCode.Models.Server/AyCode.Models.Server.csproj
index a7e9dae..af9ef0b 100644
--- a/AyCode.Models.Server/AyCode.Models.Server.csproj
+++ b/AyCode.Models.Server/AyCode.Models.Server.csproj
@@ -20,6 +20,7 @@
+
diff --git a/AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs b/AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs
new file mode 100644
index 0000000..f43f128
--- /dev/null
+++ b/AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs
@@ -0,0 +1,35 @@
+using System.Collections.Concurrent;
+using System.Reflection;
+using AyCode.Services.SignalRs;
+
+namespace AyCode.Models.Server.DynamicMethods;
+
+public class AcDynamicMethodCallModel where TAttribute : TagAttribute
+{
+ public object InstanceObject { get; init; }
+ public ConcurrentDictionary> MethodsByMessageTag { get; init; } = new();
+
+
+ public AcDynamicMethodCallModel(Type instanceObjectType) : this(instanceObjectType, null!)
+ {
+ }
+
+ public AcDynamicMethodCallModel(Type instanceObjectType, params object[] constructorParams) : this(Activator.CreateInstance(instanceObjectType, constructorParams)!)
+ {
+ }
+
+ public AcDynamicMethodCallModel(object instanceObject)
+ {
+ InstanceObject = instanceObject;
+
+ foreach (var methodInfo in instanceObject.GetType().GetMethods())
+ {
+ if (methodInfo.GetCustomAttribute(typeof(TAttribute)) is not TAttribute attribute) continue;
+
+ if (MethodsByMessageTag.ContainsKey(attribute.MessageTag))
+ throw new Exception($"Multiple SignaRMessageTag! messageTag: {attribute.MessageTag}; methodName: {methodInfo.Name}");
+
+ MethodsByMessageTag[attribute.MessageTag] = new AcMethodInfoModel(attribute, methodInfo!);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AyCode.Models.Server/DynamicMethods/AcMethodInfoModel.cs b/AyCode.Models.Server/DynamicMethods/AcMethodInfoModel.cs
new file mode 100644
index 0000000..0fd4c34
--- /dev/null
+++ b/AyCode.Models.Server/DynamicMethods/AcMethodInfoModel.cs
@@ -0,0 +1,24 @@
+using System.Reflection;
+using AyCode.Services.SignalRs;
+
+namespace AyCode.Models.Server.DynamicMethods;
+
+public class AcMethodInfoModel where TAttribute : TagAttribute
+{
+ public ParameterInfo[]? ParamInfos { get; init; } = null;
+ public TAttribute Attribute { get; init; }
+ public MethodInfo MethodInfo { get; init; }
+
+ public AcMethodInfoModel(TAttribute attribute, MethodInfo methodInfo)
+ {
+ Attribute = attribute;
+ MethodInfo = methodInfo;
+
+ var parameters = methodInfo.GetParameters();
+
+ //if (parameters.Length > 1)
+ // throw new Exception("MethodInfoModel; parameters.Length > 1");
+
+ ParamInfos = parameters;
+ }
+}
\ No newline at end of file
diff --git a/AyCode.Services.Server/AyCode.Services.Server.csproj b/AyCode.Services.Server/AyCode.Services.Server.csproj
index 9b02523..f763444 100644
--- a/AyCode.Services.Server/AyCode.Services.Server.csproj
+++ b/AyCode.Services.Server/AyCode.Services.Server.csproj
@@ -10,6 +10,9 @@
+
+
+
diff --git a/AyCode.Services.Server/SignalRs/AcSessionService.cs b/AyCode.Services.Server/SignalRs/AcSessionService.cs
new file mode 100644
index 0000000..6cb8aee
--- /dev/null
+++ b/AyCode.Services.Server/SignalRs/AcSessionService.cs
@@ -0,0 +1,12 @@
+using System.Collections.Concurrent;
+
+namespace AyCode.Services.Server.SignalRs;
+
+public class AcSessionService where TSessionItem : IAcSessionItem where TSessionItemId : notnull
+{
+ public ConcurrentDictionary Sessions { get; private set; } = [];
+
+ public AcSessionService()
+ {
+ }
+}
\ No newline at end of file
diff --git a/AyCode.Services.Server/SignalRs/AcSignalRClientBase.cs b/AyCode.Services.Server/SignalRs/AcSignalRClientBase.cs
new file mode 100644
index 0000000..eb8908d
--- /dev/null
+++ b/AyCode.Services.Server/SignalRs/AcSignalRClientBase.cs
@@ -0,0 +1,283 @@
+using System.Collections.Concurrent;
+using AyCode.Core;
+using AyCode.Core.Extensions;
+using AyCode.Core.Helpers;
+using AyCode.Core.Loggers;
+using AyCode.Interfaces.Entities;
+using AyCode.Services.SignalRs;
+using MessagePack.Resolvers;
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace AyCode.Services.Server.SignalRs
+{
+ public abstract class AcSignalRClientBase : IAcSignalRHubClient
+ {
+ private readonly ConcurrentDictionary _responseByRequestId = new();
+
+ protected readonly HubConnection HubConnection;
+ protected readonly AcLoggerBase Logger;
+
+ public event Action OnMessageReceived = null!;
+ //public event Action OnMessageRequested;
+
+ public int Timeout = 10000;
+ private const string TagsName = "SignalRTags";
+
+ protected AcSignalRClientBase(string fullHubName, AcLoggerBase logger)
+ {
+ Logger = logger;
+
+ HubConnection = new HubConnectionBuilder()
+ .WithUrl(fullHubName)
+ //.AddMessagePackProtocol(options => {
+ // options.SerializerOptions = MessagePackSerializerOptions.Standard
+ // .WithResolver(MessagePack.Resolvers.StandardResolver.Instance)
+ // .WithSecurity(MessagePackSecurity.UntrustedData)
+ // .WithCompression(MessagePackCompression.Lz4Block)
+ // .WithCompressionMinLength(256);})
+ .Build();
+
+ HubConnection.Closed += HubConnection_Closed;
+
+ _ = HubConnection.On(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage);
+ //_ = HubConnection.On(nameof(IAcSignalRHubClient.OnRequestMessage), OnRequestMessage);
+
+ HubConnection.StartAsync().Forget();
+ }
+
+ private Task HubConnection_Closed(Exception? arg)
+ {
+ if (_responseByRequestId.IsEmpty) Logger.DebugConditional($"Client HubConnection_Closed");
+ else Logger.Warning($"Client HubConnection_Closed; {nameof(_responseByRequestId)} count: {_responseByRequestId.Count}");
+
+ _responseByRequestId.Clear();
+ return Task.CompletedTask;
+ }
+
+ public async Task StartConnection()
+ {
+ if (HubConnection.State == HubConnectionState.Disconnected)
+ await HubConnection.StartAsync();
+
+ if (HubConnection.State != HubConnectionState.Connected)
+ await TaskHelper.WaitToAsync(() => HubConnection.State == HubConnectionState.Connected, Timeout, 10, 25);
+ }
+
+ public async Task StopConnection()
+ {
+ await HubConnection.StopAsync();
+ await HubConnection.DisposeAsync();
+ }
+
+ public virtual Task SendMessageToServerAsync(int messageTag)
+ => SendMessageToServerAsync(messageTag, null, AcDomain.NextUniqueInt32);
+
+ public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId)
+ {
+ Logger.DebugConditional($"Client SendMessageToServerAsync; {nameof(requestId)}: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
+
+ return StartConnection().ContinueWith(_ =>
+ {
+ var msgp = message?.ToMessagePack(ContractlessStandardResolver.Options);
+ return HubConnection.SendAsync(nameof(IAcSignalRHubClient.OnReceiveMessage), messageTag, msgp, requestId);
+ });
+ }
+
+ #region CRUD
+ public virtual Task GetByIdAsync(int messageTag, object id) //where TResponseData : class
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(id)), AcDomain.NextUniqueInt32);
+ public virtual Task GetByIdAsync(int messageTag, Func, Task> responseCallback, object id)
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(id)), responseCallback);
+
+ public virtual Task GetByIdAsync(int messageTag, object[] ids) //where TResponseData : class
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(ids)), AcDomain.NextUniqueInt32);
+ public virtual Task GetByIdAsync(int messageTag, Func, Task> responseCallback, object[] ids)
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(ids)), responseCallback);
+
+ public virtual Task GetAllAsync(int messageTag) //where TResponseData : class
+ => SendMessageToServerAsync(messageTag);
+ public virtual Task GetAllAsync(int messageTag, Func, Task> responseCallback)
+ => SendMessageToServerAsync(messageTag, null, responseCallback);
+
+ public virtual Task GetAllAsync(int messageTag, Func, Task> responseCallback, object[]? contextParams)
+ => SendMessageToServerAsync(messageTag, (contextParams == null || contextParams.Length == 0 ? null : new SignalPostJsonDataMessage(new IdMessage(contextParams))), responseCallback);
+ public virtual Task GetAllAsync(int messageTag, object[]? contextParams) //where TResponseData : class
+ => SendMessageToServerAsync(messageTag, contextParams == null || contextParams.Length == 0 ? null : new SignalPostJsonDataMessage(new IdMessage(contextParams)), AcDomain.NextUniqueInt32);
+
+ public virtual Task PostDataAsync(int messageTag, TPostData postData) where TPostData : class
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(postData), AcDomain.NextUniqueInt32);
+ public virtual Task PostDataAsync(int messageTag, TPostData postData) //where TPostData : class where TResponseData : class
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(postData), AcDomain.NextUniqueInt32);
+
+ public virtual Task PostDataAsync(int messageTag, TPostData postData, Func, Task> responseCallback) //where TPostData : class
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(postData), responseCallback);
+ public virtual Task PostDataAsync(int messageTag, TPostData postData, Func, Task> responseCallback) //where TPostData : class where TResponseData : class
+ => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(postData), responseCallback);
+
+ public Task GetAllIntoAsync(List intoList, int messageTag, object[]? contextParams = null, Action? callback = null) where TResponseItem : IEntityGuid
+ {
+ return GetAllAsync>(messageTag, response =>
+ {
+ var logText = $"GetAllIntoAsync<{typeof(TResponseItem).Name}>(); status: {response.Status}; dataCount: {response.ResponseData?.Count}; {ConstHelper.NameByValue(TagsName, messageTag)};";
+
+ intoList.Clear();
+
+ if (response.Status == SignalResponseStatus.Success && response.ResponseData != null)
+ {
+ Logger.Debug(logText);
+ intoList.AddRange(response.ResponseData);
+ }
+ else Logger.Error(logText);
+
+ callback?.Invoke();
+ return Task.CompletedTask;
+ }, contextParams);
+ }
+
+ #endregion CRUD
+
+ public virtual Task SendMessageToServerAsync(int messageTag) //where TResponse : class
+ => SendMessageToServerAsync(messageTag, null, AcDomain.NextUniqueInt32);
+
+ public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message) //where TResponse : class
+ => SendMessageToServerAsync(messageTag, message, AcDomain.NextUniqueInt32);
+
+ protected virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int requestId) //where TResponse : class
+ {
+ Logger.DebugConditional($"Client SendMessageToServerAsync; {nameof(requestId)}: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
+
+ _responseByRequestId[requestId] = new SignalRRequestModel();
+ await SendMessageToServerAsync(messageTag, message, requestId);
+
+ try
+ {
+ if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId].ResponseByRequestId != null, Timeout, 25, 50) &&
+ _responseByRequestId.TryRemove(requestId, out var obj) && obj.ResponseByRequestId is ISignalResponseMessage responseMessage)
+ {
+ if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null)
+ {
+ var errorText = $"Client SendMessageToServerAsync response error; await; tag: {messageTag}; Status: {responseMessage.Status}; requestId: {requestId};";
+
+ Logger.Error(errorText);
+
+ //TODO: Ideiglenes, majd a ResponseMessage-et kell visszaadni a Status miatt! - J.
+ return await Task.FromException(new Exception(errorText));
+
+ //throw new Exception(errorText);
+ //return default;
+ }
+
+ return responseMessage.ResponseData.JsonTo();
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Error($"SendMessageToServerAsync; requestId: {requestId}; {ex.Message}; {ConstHelper.NameByValue(TagsName, messageTag)}", ex);
+ }
+
+ _responseByRequestId.TryRemove(requestId, out _);
+ return default;
+ }
+
+ public virtual Task SendMessageToServerAsync(int messageTag, Func, Task> responseCallback)
+ => SendMessageToServerAsync(messageTag, null, responseCallback);
+
+ public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, Func, Task> responseCallback)
+ {
+ if (messageTag == 0)
+ Logger.Error($"SendMessageToServerAsync; messageTag == 0");
+
+ var requestId = AcDomain.NextUniqueInt32;
+
+ _responseByRequestId[requestId] = new SignalRRequestModel(new Action>(responseMessage =>
+ {
+ TResponseData? responseData = default;
+
+ if (responseMessage.Status == SignalResponseStatus.Success)
+ {
+ responseData = string.IsNullOrEmpty(responseMessage.ResponseData) ? default : responseMessage.ResponseData.JsonTo();
+ }
+ else Logger.Error($"Client SendMessageToServerAsync response error; callback; Status: {responseMessage.Status}; requestId: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}");
+
+ responseCallback(new SignalResponseMessage(messageTag, responseMessage.Status, responseData));
+ }));
+
+ return SendMessageToServerAsync(messageTag, message, requestId);
+ }
+
+ public virtual Task OnReceiveMessage(int messageTag, byte[] message, int? requestId)
+ {
+ var logText = $"Client OnReceiveMessage; {nameof(requestId)}: {requestId}; {ConstHelper.NameByValue(TagsName, messageTag)}";
+
+ if (message.Length == 0) Logger.Warning($"message.Length == 0! {logText}");
+
+ try
+ {
+ if (requestId.HasValue && _responseByRequestId.ContainsKey(requestId.Value))
+ {
+ var reqId = requestId.Value;
+
+ _responseByRequestId[reqId].ResponseDateTime = DateTime.UtcNow;
+ Logger.Info($"[{_responseByRequestId[reqId].ResponseDateTime.Subtract(_responseByRequestId[reqId].RequestDateTime).TotalMilliseconds:N0}ms][{(message.Length/1024)}kb]{logText}");
+
+ var responseMessage = message.MessagePackTo(ContractlessStandardResolver.Options);
+
+ switch (_responseByRequestId[reqId].ResponseByRequestId)
+ {
+ case null:
+ _responseByRequestId[reqId].ResponseByRequestId = responseMessage;
+ return Task.CompletedTask;
+
+ case Action> messagePackCallback:
+ _responseByRequestId.TryRemove(reqId, out _);
+
+ messagePackCallback.Invoke(responseMessage);
+ return Task.CompletedTask;
+
+ //case Action jsonCallback:
+ // _responseByRequestId.TryRemove(reqId, out _);
+
+ // jsonCallback.Invoke(responseMessage);
+ // return Task.CompletedTask;
+
+ default:
+ Logger.Error($"Client OnReceiveMessage switch; unknown message type: {_responseByRequestId[reqId].ResponseByRequestId?.GetType().Name}; {ConstHelper.NameByValue(TagsName, messageTag)}");
+ break;
+ }
+
+ _responseByRequestId.TryRemove(reqId, out _);
+ }
+ else Logger.Info(logText);
+
+ OnMessageReceived(messageTag, message, requestId);
+ }
+ catch (Exception ex)
+ {
+ if (requestId.HasValue)
+ _responseByRequestId.TryRemove(requestId.Value, out _);
+
+ Logger.Error($"Client OnReceiveMessage; requestId: {requestId}; {ex.Message}; {ConstHelper.NameByValue(TagsName, messageTag)}", ex);
+ throw;
+ }
+
+ return Task.CompletedTask;
+ }
+ //public virtual Task OnRequestMessage(int messageTag, int requestId)
+ //{
+ // Logger.DebugConditional($"Client OnRequestMessage; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId};");
+
+ // try
+ // {
+ // OnMessageRequested(messageTag, requestId);
+ // }
+ // catch(Exception ex)
+ // {
+ // Logger.Error($"Client OnReceiveMessage; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId}; {ex.Message}", ex);
+ // throw;
+ // }
+
+ // return Task.CompletedTask;
+
+ //}
+ }
+}
diff --git a/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs
new file mode 100644
index 0000000..9d1b870
--- /dev/null
+++ b/AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs
@@ -0,0 +1,1075 @@
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using AyCode.Core.Enums;
+using AyCode.Core.Extensions;
+using AyCode.Core.Helpers;
+using AyCode.Core.Interfaces;
+using AyCode.Services.SignalRs;
+
+namespace AyCode.Services.Server.SignalRs
+{
+ public class TrackingItem(TrackingState trackingState, T currentValue, T? originalValue = null) where T : class, IId
+ {
+ public TrackingState TrackingState { get; internal set; } = trackingState;
+ public T CurrentValue { get; internal set; } = currentValue;
+ public T? OriginalValue { get; init; } = originalValue;
+
+ internal TrackingItem UpdateItem(TrackingState trackingState, T newValue)
+ {
+ CurrentValue = newValue;
+
+ if (TrackingState != TrackingState.Add)
+ TrackingState = trackingState;
+
+ return this;
+ }
+ }
+
+
+ public class ChangeTracking /*: IEnumerable>*/ where T : class, IId
+ {
+ private readonly List> _trackingItems = []; //TODO: Dictionary... - J.
+
+ internal TrackingItem? AddTrackingItem(TrackingState trackingState, T newValue, T? originalValue = null)
+ {
+ if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), $@"currentValue.Id.IsNullOrEmpty()");
+
+ var itemIndex = _trackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id);
+ TrackingItem? trackingItem = null;
+
+ if (itemIndex > -1)
+ {
+ trackingItem = _trackingItems[itemIndex];
+
+ if (trackingState == TrackingState.Remove && trackingItem.TrackingState == TrackingState.Add)
+ {
+ _trackingItems.RemoveAt(itemIndex);
+ return null;
+ }
+
+ return trackingItem.UpdateItem(trackingState, newValue);
+
+ }
+
+ if (originalValue != null && Equals(newValue, originalValue))
+ originalValue = TrackingItemHelpers.JsonClone(originalValue);
+
+ trackingItem = new TrackingItem(trackingState, newValue, originalValue);
+ _trackingItems.Add(trackingItem);
+
+ return trackingItem;
+ }
+
+ public int Count => _trackingItems.Count;
+ internal void Clear() => _trackingItems.Clear();
+ public List> ToList() => _trackingItems.ToList();
+
+ public bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem)
+ {
+ trackingItem = _trackingItems.FirstOrDefault(x => x.CurrentValue.Id == id);
+ return trackingItem != null;
+ }
+
+ internal void Remove(TrackingItem trackingItem) => _trackingItems.Remove(trackingItem);
+
+ //public IEnumerator> GetEnumerator()
+ //{
+ // return _trackingItems.GetEnumerator();
+ //}
+
+ //IEnumerator IEnumerable.GetEnumerator()
+ //{
+ // return GetEnumerator();
+ //}
+ }
+
+
+
+ [Serializable]
+ [DebuggerDisplay("Count = {Count}")]
+ public class AcSignalRDataSource : IList, IList, IReadOnlyList where T : class, IId
+ {
+ private readonly object _syncRoot = new();
+
+ protected List InnerList = []; //TODO: Dictionary??? - J.
+ protected readonly ChangeTracking TrackingItems = new();
+
+ public object[]? ContextIds;
+ public string? FilterText { get; set; }
+
+ public AcSignalRClientBase SignalRClient;
+ protected readonly SignalRCrudTags SignalRCrudTags;
+
+ public Func, Task>? OnDataSourceItemChanged;
+ public Func? OnDataSourceLoaded;
+
+ public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null)
+ {
+ //if (contextIds != null) (ContextIds = new List