From d1ff3af3658952d7e8c0c9453813e367885b9e88 Mon Sep 17 00:00:00 2001 From: Loretta Date: Sun, 31 Aug 2025 12:39:47 +0200 Subject: [PATCH] Add SignalR common services and dependencies --- .../AyCode.Models.Server.csproj | 1 + .../AcDynamicMethodCallModel.cs | 35 + .../DynamicMethods/AcMethodInfoModel.cs | 24 + .../AyCode.Services.Server.csproj | 3 + .../SignalRs/AcSessionService.cs | 12 + .../SignalRs/AcSignalRClientBase.cs | 283 +++++ .../SignalRs/AcSignalRDataSource.cs | 1075 +++++++++++++++++ .../SignalRs/AcWebSignalRHubBase.cs | 195 +++ .../SignalRs/ExtensionMethods.cs | 18 + .../SignalRs/IAcSessionItem.cs | 6 + .../SignalRs/SignalRRequestModel.cs | 19 + .../SignalRs/TrackingItemHelpers.cs | 46 + 12 files changed, 1717 insertions(+) create mode 100644 AyCode.Models.Server/DynamicMethods/AcDynamicMethodCallModel.cs create mode 100644 AyCode.Models.Server/DynamicMethods/AcMethodInfoModel.cs create mode 100644 AyCode.Services.Server/SignalRs/AcSessionService.cs create mode 100644 AyCode.Services.Server/SignalRs/AcSignalRClientBase.cs create mode 100644 AyCode.Services.Server/SignalRs/AcSignalRDataSource.cs create mode 100644 AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs create mode 100644 AyCode.Services.Server/SignalRs/ExtensionMethods.cs create mode 100644 AyCode.Services.Server/SignalRs/IAcSessionItem.cs create mode 100644 AyCode.Services.Server/SignalRs/SignalRRequestModel.cs create mode 100644 AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs 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()).AddRange(contextIds); + ContextIds = contextIds; + + SignalRCrudTags = signalRCrudTags; + SignalRClient = signalRClient; + } + + public bool IsSynchronized => true; + public object SyncRoot => _syncRoot; + public bool IsFixedSize => false; + + public bool HasWorkingReferenceList { get; private set; } + + public void SetWorkingReferenceList(List workingList) + { + if (workingList == null!) return; //throw new ArgumentNullException(nameof(workingList)); + + Monitor.Enter(_syncRoot); + + try + { + HasWorkingReferenceList = true; + if (ReferenceEquals(InnerList, workingList)) return; + + if (workingList.Count == 0) workingList.AddRange(InnerList); + + Clear(true); + InnerList = workingList; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + private object[]? GetContextParams() + { + var parameters = new List(); + if (ContextIds != null) parameters.AddRange(ContextIds); + if (FilterText != null) parameters.Add(FilterText); //Az empty string-et beletesszük, h legyen paraméter! - J. + + if (parameters.Count == 0) parameters = null; + return parameters?.ToArray(); + } + + /// + /// GetAllMessageTag + /// + /// + /// + public async Task LoadDataSource(bool clearChangeTracking = true) + { + if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); + + var responseData = (await SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, GetContextParams())) ?? throw new NullReferenceException(); + + await LoadDataSource(responseData, false, false, clearChangeTracking); + } + + public Task LoadDataSourceAsync(bool clearChangeTracking = true) + { + if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); + + return SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, result=> + { + if (result.Status != SignalResponseStatus.Success || result.ResponseData == null) + throw new NullReferenceException($"LoadDataSourceAsync; result.Status != SignalResponseStatus.Success || result.ResponseData == null; Status: {SignalResponseStatus.Success}"); + + return LoadDataSource(result.ResponseData, false, false, clearChangeTracking); + }, GetContextParams()); + } + + public async Task LoadDataSource(IList fromSource, bool refreshDataFromDbAsync = false, bool setSourceToWorkingReferenceList = false, bool clearChangeTracking = true) + { + Monitor.Enter(_syncRoot); + + try + { + if (!ReferenceEquals(InnerList, fromSource)) + { + Clear(clearChangeTracking); + + if (setSourceToWorkingReferenceList && fromSource is List fromSourceList) SetWorkingReferenceList(fromSourceList); + else InnerList.AddRange(fromSource); + } + else if (clearChangeTracking) TrackingItems.Clear(); + + if (refreshDataFromDbAsync) LoadDataSourceAsync(false).Forget(); + } + finally + { + Monitor.Exit(_syncRoot); + } + + if (OnDataSourceLoaded != null) await OnDataSourceLoaded.Invoke(); + } + + public async Task LoadItem(Guid id) + { + if (SignalRCrudTags.GetItemMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetItemMessageTag == SignalRTags.None"); + + T? resultitem = null; + + Monitor.Enter(_syncRoot); + + try + { + resultitem = await SignalRClient.GetByIdAsync(SignalRCrudTags.GetItemMessageTag, id); + if (resultitem == null) return null; + + if (TryGetIndex(id, out var index)) InnerList[index] = resultitem; + else InnerList.Add(resultitem); + + var eventArgs = new ItemChangedEventArgs(resultitem, TrackingState.Get); + if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs); + } + finally + { + Monitor.Exit(_syncRoot); + } + + return resultitem; + } + + /// + /// set: UpdateMessageTag + /// + /// + /// + /// + public T this[int index] + { + get + { + if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); + + Monitor.Enter(_syncRoot); + try + { + return InnerList[index]; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + set + { + Monitor.Enter(_syncRoot); + try + { + UpdateUnsafe(index, value); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + } + + public void Add(T newValue) + { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Add->newValue.Id.IsNullOrEmpty()"); + + Monitor.Enter(_syncRoot); + + try + { + if (Contains(newValue)) + throw new ArgumentException($@"It already contains this Id! Id: {newValue.Id}", nameof(newValue)); + + UnsafeAdd(newValue); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// AddMessageTag + /// + /// + /// + /// + public async Task Add(T newValue, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + Add(newValue); + + return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// AddMessageTag or UpdateMessageTag + /// + /// + /// + /// + public async Task AddOrUpdate(T newValue, bool autoSave) + { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"AddOrUpdate->newValue.Id.IsNullOrEmpty()"); + + Monitor.Enter(_syncRoot); + + try + { + var index = IndexOf(newValue); + + return index > -1 ? await Update(index, newValue, autoSave) : await Add(newValue, autoSave); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + //public void AddRange(IEnumerable collection) + //{ + // lock (_syncRoot) + // { + + // } + //} + + protected void UnsafeAdd(T newValue) + { + TrackingItems.AddTrackingItem(TrackingState.Add, newValue); + InnerList.Add(newValue); + } + + /// + /// AddMessageTag + /// + /// + /// + /// + /// + /// + public void Insert(int index, T newValue) + { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()"); + + Monitor.Enter(_syncRoot); + + try + { + if (Contains(newValue)) + throw new ArgumentException($@"Insert; It already contains this Id! Id: {newValue.Id}", nameof(newValue)); + + TrackingItems.AddTrackingItem(TrackingState.Add, newValue); + InnerList.Insert(index, newValue); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task Insert(int index, T newValue, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + Insert(index, newValue); + + return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + /// + /// UpdateMessageTag + /// + /// + /// + public Task Update(T newItem, bool autoSave) => Update(IndexOf(newItem), newItem, autoSave); + + /// + /// UpdateMessageTag + /// + /// + /// + /// + /// /// + /// /// + /// + /// + public async Task Update(int index, T newValue, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + UpdateUnsafe(index, newValue); + + return autoSave ? await SaveItem(newValue, TrackingState.Update) : newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + private void UpdateUnsafe(int index, T newValue) + { + if (default(T) != null && newValue == null) throw new NullReferenceException(nameof(newValue)); + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"UpdateUnsafe->newValue.Id.IsNullOrEmpty()"); + if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); + + Monitor.Enter(_syncRoot); + + try + { + var currentItem = InnerList[index]; + + if (currentItem.Id != newValue.Id) + throw new ArgumentException($@"UpdateUnsafe; currentItem.Id != item.Id! Id: {newValue.Id}", nameof(newValue)); + + TrackingItems.AddTrackingItem(TrackingState.Update, newValue, currentItem); + InnerList[index] = newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + + /// + /// RemoveMessageTag + /// + /// + /// + public bool Remove(T item) + { + Monitor.Enter(_syncRoot); + + try + { + var index = IndexOf(item); + + if (index < 0) return false; + + RemoveAt(index); + return true; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task Remove(T item, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + var result = Remove(item); + + if (!autoSave || !result) return result; + + await SaveItem(item, TrackingState.Remove); + return true; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + /// + /// + /// + /// + /// + /// + public bool TryRemove(Guid id, out T? item) + { + Monitor.Enter(_syncRoot); + + try + { + return TryGetValue(id, out item) && Remove(item); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + /// + /// RemoveMessageTag + /// + /// + /// + /// /// + /// + public void RemoveAt(int index) + { + Monitor.Enter(_syncRoot); + + try + { + var currentItem = InnerList[index]; + if (currentItem.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(currentItem), $@"RemoveAt->item.Id.IsNullOrEmpty(); index: {index}"); + + TrackingItems.AddTrackingItem(TrackingState.Remove, currentItem, currentItem); + InnerList.RemoveAt(index); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task RemoveAt(int index, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + var currentItem = InnerList[index]; + RemoveAt(index); + + if (autoSave) + { + await SaveItem(currentItem, TrackingState.Remove); + } + } + finally + { + Monitor.Exit(_syncRoot); + } + } + /// + /// + /// + /// + public List> GetTrackingItems() + { + Monitor.Enter(_syncRoot); + + try + { + return TrackingItems.ToList(); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public void SetTrackingStateToUpdate(T item) + { + Monitor.Enter(_syncRoot); + + try + { + if (TrackingItems.TryGetTrackingItem(item.Id, out var trackingItem)) + { + if (trackingItem.TrackingState != TrackingState.Add) + trackingItem.TrackingState = TrackingState.Update; + + return; + } + + if (!TryGetValue(item.Id, out var originalItem)) return; + TrackingItems.AddTrackingItem(TrackingState.Update, item, originalItem); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// + /// + /// + /// + /// + public bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem) + { + Monitor.Enter(_syncRoot); + + try + { + return TrackingItems.TryGetTrackingItem(id, out trackingItem); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + /// + /// + /// + /// Unsaved items + public async Task>> SaveChanges() + { + Monitor.Enter(_syncRoot); + + try + { + foreach (var trackingItem in TrackingItems.ToList()) + { + try + { + await SaveTrackingItemUnsafe(trackingItem); + } + catch(Exception ex) + { + TryRollbackItem(trackingItem.CurrentValue.Id, out _); + } + } + + return TrackingItems.ToList(); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task SaveChangesAsync() + { + Monitor.Enter(_syncRoot); + + try + { + foreach (var trackingItem in TrackingItems.ToList()) + { + try + { + await SaveTrackingItemUnsafeAsync(trackingItem); + } + catch(Exception ex) + { + TryRollbackItem(trackingItem.CurrentValue.Id, out _); + } + } + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// + /// + /// + /// + public async Task SaveItem(Guid id) + { + Monitor.Enter(_syncRoot); + + try + { + T resultItem = null!; + + if (TryGetTrackingItem(id, out var trackingItem)) + resultItem = await SaveTrackingItemUnsafe(trackingItem); + + if (resultItem == null) throw new NullReferenceException($"SaveItem; resultItem == null"); + return resultItem; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task SaveItem(Guid id, TrackingState trackingState) + { + //Monitor.Enter(_syncRoot); + + try + { + T resultItem = null!; + + if (TryGetValue(id, out var item)) + resultItem = await SaveItem(item, trackingState); + + if (resultItem == null) throw new NullReferenceException($"SaveItem; resultItem == null"); + return resultItem; + } + finally + { + //Monitor.Exit(_syncRoot); + } + } + + public Task SaveItem(T item, TrackingState trackingState) => SaveItemUnsafe(item, trackingState); + + protected Task SaveTrackingItemUnsafe(TrackingItem trackingItem) + => SaveItemUnsafe(trackingItem.CurrentValue, trackingItem.TrackingState); + + protected Task SaveTrackingItemUnsafeAsync(TrackingItem trackingItem) + => SaveItemUnsafeAsync(trackingItem.CurrentValue, trackingItem.TrackingState); + + protected Task SaveItemUnsafe(T item, TrackingState trackingState) + { + var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); + if (messageTag == AcSignalRTags.None) throw new ArgumentException($"SaveItemUnsafe; messageTag == SignalRTags.None"); + + return SignalRClient.PostDataAsync(messageTag, item).ContinueWith(x => + { + if (x.Result == null) + { + if (TryRollbackItem(item.Id, out _)) return item; + + throw new NullReferenceException($"SaveItemUnsafe; result == null"); + } + + ProcessSavedResponseItem(x.Result, trackingState); + return x.Result; + }); + } + + protected Task SaveItemUnsafeAsync(T item, TrackingState trackingState) + { + var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); + if (messageTag == AcSignalRTags.None) return Task.CompletedTask; + + return SignalRClient.PostDataAsync(messageTag, item, response => + { + Monitor.Enter(_syncRoot); + + try + { + if (response.Status != SignalResponseStatus.Success || response.ResponseData == null) + { + if (TryRollbackItem(item.Id, out _)) return Task.CompletedTask; + + throw new NullReferenceException($"SaveItemUnsafeAsync; result.Status != SignalResponseStatus.Success || result.ResponseData == null; Status: {SignalResponseStatus.Success}"); + } + + return ProcessSavedResponseItem(response.ResponseData, trackingState); + } + finally + { + Monitor.Exit(_syncRoot); + } + }); + } + + private Task ProcessSavedResponseItem(T? resultItem, TrackingState trackingState) + { + if (resultItem == null) return Task.CompletedTask; + + if (TryGetTrackingItem(resultItem.Id, out var trackingItem)) + TrackingItems.Remove(trackingItem); + + if (TryGetIndex(resultItem.Id, out var index)) + InnerList[index] = resultItem; + + var eventArgs = new ItemChangedEventArgs(resultItem, trackingState); + if (OnDataSourceItemChanged != null) return OnDataSourceItemChanged.Invoke(eventArgs); + + return Task.CompletedTask; + } + + protected void RollbackItemUnsafe(TrackingItem trackingItem) + { + if (TryGetIndex(trackingItem.CurrentValue.Id, out var index)) + { + if (trackingItem.TrackingState == TrackingState.Add) InnerList.RemoveAt(index); + else InnerList[index] = trackingItem.OriginalValue!; + } + else if (trackingItem.TrackingState != TrackingState.Add) + InnerList.Add(trackingItem.OriginalValue!); + + TrackingItems.Remove(trackingItem); + } + + public bool TryRollbackItem(Guid id, out T? originalValue) + { + Monitor.Enter(_syncRoot); + try + { + if (TryGetTrackingItem(id, out var trackingItem)) + { + originalValue = trackingItem.OriginalValue; + + RollbackItemUnsafe(trackingItem); + return true; + } + + originalValue = null; + return false; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public void Rollback() + { + Monitor.Enter(_syncRoot); + try + { + foreach (var trackingItem in TrackingItems.ToList()) + RollbackItemUnsafe(trackingItem); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public int Count + { + get + { + Monitor.Enter(_syncRoot); + try + { + return InnerList.Count; + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + } + + public void Clear() => Clear(true); + + public void Clear(bool clearChangeTracking) + { + Monitor.Enter(_syncRoot); + try + { + if (clearChangeTracking) TrackingItems.Clear(); + InnerList.Clear(); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public int IndexOf(Guid id) + { + Monitor.Enter(_syncRoot); + try + { + return InnerList.FindIndex(x => x.Id == id); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + public int IndexOf(T item) => IndexOf(item.Id); + public bool TryGetIndex(Guid id, out int index) => (index = IndexOf(id)) > -1; + + public bool Contains(T item) => IndexOf(item) > -1; + + public bool TryGetValue(Guid id, [NotNullWhen(true)] out T? item) + { + Monitor.Enter(_syncRoot); + try + { + item = InnerList.FirstOrDefault(x => x.Id == id); + return item != null; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public void CopyTo(T[] array) => CopyTo(array, 0); + + public void CopyTo(T[] array, int arrayIndex) + { + Monitor.Enter(_syncRoot); + try + { + InnerList.CopyTo(array, arrayIndex); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + public int BinarySearch(int index, int count, T item, IComparer? comparer) + { + throw new NotImplementedException($"BinarySearch"); + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + if (Count - index < count) + throw new ArgumentException("Invalid length"); + + //Monitor.Enter(_syncRoot); + //try + //{ + // return InnerList.BinarySearch(index, count, item, comparer); + //} + //finally + //{ + // Monitor.Exit(_syncRoot); + //} + } + + public int BinarySearch(T item) => BinarySearch(0, Count, item, null); + public int BinarySearch(T item, IComparer? comparer) => BinarySearch(0, Count, item, comparer); + + public IEnumerator GetEnumerator() + { + Monitor.Enter(_syncRoot); + try + { + //return InnerList.ToList().GetEnumerator(); + return InnerList.GetEnumerator(); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + public ReadOnlyCollection AsReadOnly() => new(this); + private static bool IsCompatibleObject(object? value) => (value is T) || (value == null && default(T) == null); + + + #region IList, ICollection + + bool IList.IsReadOnly => false; + + object? IList.this[int index] + { + get => this[index]; + set + { + if (default(T) != null && value == null) throw new NullReferenceException(nameof(value)); + + try + { + this[index] = (T)value!; + } + catch (InvalidCastException) + { + throw new InvalidCastException(nameof(value)); + } + } + } + + int IList.Add(object? item) + { + if (default(T) != null && item == null) throw new NullReferenceException(nameof(item)); + + try + { + Add((T)item!); + } + catch (InvalidCastException) + { + throw new InvalidCastException(nameof(item)); + } + + return Count - 1; + } + + void IList.Clear() => Clear(true); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + bool IList.Contains(object? item) => IsCompatibleObject(item) && Contains((T)item!); + int IList.IndexOf(object? item) => (IsCompatibleObject(item)) ? IndexOf((T)item!) : -1; + + void IList.Insert(int index, object? item) + { + if (default(T) != null && item == null) throw new NullReferenceException(nameof(item)); + + try + { + Insert(index, (T)item!); + } + catch (InvalidCastException) + { + throw new InvalidCastException(nameof(item)); + } + } + + void IList.Remove(object? item) + { + if (IsCompatibleObject(item)) Remove((T)item!); + } + + void ICollection.Clear() => Clear(true); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if ((array != null) && (array.Rank != 1)) + { + throw new ArgumentException(); + } + + try + { + Monitor.Enter(_syncRoot); + try + { + //TODO: _list.ToArray() - ez nem az igazi... - J. + Array.Copy(InnerList.ToArray(), 0, array!, arrayIndex, InnerList.Count); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + catch (ArrayTypeMismatchException) + { + throw new ArrayTypeMismatchException(); + } + } + + int ICollection.Count => Count; + int ICollection.Count => Count; + bool ICollection.IsReadOnly => false; + void IList.RemoveAt(int index) => RemoveAt(index); + int IReadOnlyCollection.Count => Count; + + #endregion IList, ICollection + } + + public class ItemChangedEventArgs where T : IId + { + internal ItemChangedEventArgs(T item, TrackingState trackingState) + { + Item = item; + TrackingState = trackingState; + } + + public T Item { get; } + public TrackingState TrackingState { get; } + } +} diff --git a/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs new file mode 100644 index 0000000..3628316 --- /dev/null +++ b/AyCode.Services.Server/SignalRs/AcWebSignalRHubBase.cs @@ -0,0 +1,195 @@ +using System.Linq.Expressions; +using System.Security.Claims; +using AyCode.Core; +using AyCode.Core.Extensions; +using AyCode.Core.Helpers; +using AyCode.Core.Loggers; +using AyCode.Models.Server.DynamicMethods; +using AyCode.Services.SignalRs; +using MessagePack; +using MessagePack.Resolvers; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Configuration; + +namespace AyCode.Services.Server.SignalRs; + +public abstract class AcWebSignalRHubBase(IConfiguration configuration, TLogger logger) + : Hub, IAcSignalRHubServer where TSignalRTags : AcSignalRTags where TLogger : AcLoggerBase +{ + protected readonly List> DynamicMethodCallModels = []; + //protected readonly TIAM.Core.Loggers.Logger> Logger = new(logWriters.ToArray()); + protected TLogger Logger = logger; + protected IConfiguration Configuration = configuration; + + //private readonly ServiceProviderAPIController _serviceProviderApiController; + //private readonly TransferDataAPIController _transferDataApiController; + + //_serviceProviderApiController = serviceProviderApiController; + //_transferDataApiController = transferDataApiController; + + // https://docs.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-3.1#strongly-typed-hubs + public override async Task OnConnectedAsync() + { + Logger.Debug($"Server OnConnectedAsync; ConnectionId: {Context.ConnectionId}; UserIdentifier: {Context.UserIdentifier}"); + + LogContextUserNameAndId(); + + await base.OnConnectedAsync(); + + //Clients.Caller.ConnectionId = Context.ConnectionId; + //Clients.Caller.UserIdentifier = Context.UserIdentifier; + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + var logText = $"Server OnDisconnectedAsync; ConnectionId: {Context.ConnectionId}; UserIdentifier: {Context.UserIdentifier};"; + + if (exception == null) Logger.Debug(logText); + else Logger.Error(logText, exception); + + LogContextUserNameAndId(); + + await base.OnDisconnectedAsync(exception); + } + + public virtual Task OnReceiveMessage(int messageTag, byte[]? message, int? requestId) + { + return ProcessOnReceiveMessage(messageTag, message, requestId, null); + } + + protected async Task ProcessOnReceiveMessage(int messageTag, byte[]? message, int? requestId, Func? notFoundCallback) + { + var tagName = ConstHelper.NameByValue(messageTag); + var logText = $"Server OnReceiveMessage; {nameof(requestId)}: {requestId}; ConnectionId: {Context.ConnectionId}; {tagName}"; + + if (message is { Length: 0 }) Logger.Warning($"message.Length == 0! {logText}"); + else Logger.Info($"[{message?.Length:N0}b] {logText}"); + + try + { + if (AcDomain.IsDeveloperVersion) LogContextUserNameAndId(); + + foreach (var methodsByDeclaringObject in DynamicMethodCallModels) + { + if (!methodsByDeclaringObject.MethodsByMessageTag.TryGetValue(messageTag, out var methodInfoModel)) continue; + + object[]? paramValues = null; + + logText = $"Found dynamic method for the tag! method: {methodsByDeclaringObject.InstanceObject.GetType().Name}.{methodInfoModel.MethodInfo.Name}"; + + if (methodInfoModel.ParamInfos is { Length: > 0 }) + { + Logger.Debug($"{logText}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}"); + + paramValues = new object[methodInfoModel.ParamInfos.Length]; + + var firstParamType = methodInfoModel.ParamInfos[0].ParameterType; + if (methodInfoModel.ParamInfos.Length > 1 || firstParamType == typeof(string) || firstParamType.IsEnum || firstParamType.IsValueType || firstParamType == typeof(DateTime)) + { + var msg = message!.MessagePackTo>(); + + for (var i = 0; i < msg.PostData.Ids.Count; i++) + { + //var obj = (string)msg.PostData.Ids[i]; + //if (msg.PostData.Ids[i] is Guid id) + //{ + // if (id.IsNullOrEmpty()) throw new NullReferenceException($"PostData.Id.IsNullOrEmpty(); Ids: {msg.PostData.Ids}"); + // paramValues[i] = id; + //} + //else if (Guid.TryParse(obj, out id)) + //{ + // if (id.IsNullOrEmpty()) throw new NullReferenceException($"PostData.Id.IsNullOrEmpty(); Ids: {msg.PostData.Ids}"); + // paramValues[i] = id; + //} + //else if (Enum.TryParse(methodInfoModel.ParameterType, obj, out var enumObj)) + //{ + // paramValues[i] = enumObj; + //} + //else paramValues[i] = Convert.ChangeType(obj, methodInfoModel.ParameterType); + + var obj = msg.PostData.Ids[i]; + //var config = new MapperConfiguration(cfg => + //{ + // cfg.CreateMap(obj.GetType(), methodInfoModel.ParameterType); + //}); + + //var mapper = new Mapper(config); + //paramValues[i] = mapper.Map(obj, methodInfoModel.ParameterType); + + //paramValues[i] = obj; + + var a = Array.CreateInstance(methodInfoModel.ParamInfos[i].ParameterType, 1); + + if (methodInfoModel.ParamInfos[i].ParameterType == typeof(Expression)) + { + //var serializer = new ExpressionSerializer(new JsonSerializer()); + //paramValues[i] = serializer.DeserializeText((string)(obj.JsonTo(a.GetType()) as Array)?.GetValue(0)!); + } + else paramValues[i] = (obj.JsonTo(a.GetType()) as Array)?.GetValue(0)!; + + } + } + else paramValues[0] = message!.MessagePackTo>(MessagePackSerializerOptions.Standard).PostDataJson.JsonTo(firstParamType)!; + } + else Logger.Debug($"{logText}(); {tagName}"); + + var responseDataJson = new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, methodInfoModel.MethodInfo.InvokeMethod(methodsByDeclaringObject.InstanceObject, paramValues)); + var responseDataJsonKiloBytes = System.Text.Encoding.Unicode.GetByteCount(responseDataJson.ResponseData!) / 1024; + + //File.WriteAllText(Path.Combine("h:", $"{requestId}.json"), responseDataJson.ResponseData); + + Logger.Info($"[{responseDataJsonKiloBytes}kb] responseData serialized to json"); + await ResponseToCaller(messageTag, responseDataJson, requestId); + + return; + } + + Logger.Debug($"Not found dynamic method for the tag! {tagName}"); + notFoundCallback?.Invoke(tagName); + } + catch (Exception ex) + { + Logger.Error($"Server OnReceiveMessage; {ex.Message}; {tagName}", ex); + } + + await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Error), requestId); + } + + protected async Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId) + => await SendMessageToClient(Clients.Caller, messageTag, message, requestId); + + public async Task SendMessageToUserId(string userId, int messageTag, ISignalRMessage message, int? requestId) + => await SendMessageToClient(Clients.User(userId), messageTag, message, requestId); + + public async Task SendMessageToConnectionId(string connectionId, int messageTag, ISignalRMessage message, int? requestId) + => await SendMessageToClient(Clients.Client(Context.ConnectionId), messageTag, message, requestId); + + protected async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null) + { + var responseDataMessagePack = message.ToMessagePack(ContractlessStandardResolver.Options); + Logger.Info($"[{(responseDataMessagePack.Length/1024)}kb] Server sending responseDataMessagePack to client; {nameof(requestId)}: {requestId}; ConnectionId: {Context.ConnectionId}; {ConstHelper.NameByValue(messageTag)}"); + + await sendTo.OnReceiveMessage(messageTag, responseDataMessagePack, requestId); + } + + public async Task SendMessageToGroup(string groupId, int messageTag, string message) + { + //await Clients.Group(groupId).Post("", messageTag, message); + } + + //[Conditional("DEBUG")] + private void LogContextUserNameAndId() + { + string? userName = null; + var userId = Guid.Empty; + + if (Context.User != null) + { + userName = Context.User.Identity?.Name; + Guid.TryParse((string?)Context.User.FindFirstValue(ClaimTypes.NameIdentifier), out userId); + } + + if (AcDomain.IsDeveloperVersion) Logger.WarningConditional($"SignalR.Context; userName: {userName}; userId: {userId}"); + else Logger.Debug($"SignalR.Context; userName: {userName}; userId: {userId}"); + } +} \ No newline at end of file diff --git a/AyCode.Services.Server/SignalRs/ExtensionMethods.cs b/AyCode.Services.Server/SignalRs/ExtensionMethods.cs new file mode 100644 index 0000000..aa20daa --- /dev/null +++ b/AyCode.Services.Server/SignalRs/ExtensionMethods.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace AyCode.Services.Server.SignalRs; + +public static class ExtensionMethods +{ + public static object? InvokeMethod(this MethodInfo methodInfo, object obj, params object[]? parameters) + { + if (methodInfo.GetCustomAttribute(typeof(AsyncStateMachineAttribute)) is AsyncStateMachineAttribute isAsyncTask) + { + dynamic awaitable = methodInfo.Invoke(obj, parameters)!; + return awaitable.GetAwaiter().GetResult(); + } + + return methodInfo.Invoke(obj, parameters); + } +} \ No newline at end of file diff --git a/AyCode.Services.Server/SignalRs/IAcSessionItem.cs b/AyCode.Services.Server/SignalRs/IAcSessionItem.cs new file mode 100644 index 0000000..876d94e --- /dev/null +++ b/AyCode.Services.Server/SignalRs/IAcSessionItem.cs @@ -0,0 +1,6 @@ +namespace AyCode.Services.Server.SignalRs; + +public interface IAcSessionItem where TSessionItemId : notnull +{ + public TSessionItemId SessionId { get; set; } +} \ No newline at end of file diff --git a/AyCode.Services.Server/SignalRs/SignalRRequestModel.cs b/AyCode.Services.Server/SignalRs/SignalRRequestModel.cs new file mode 100644 index 0000000..727b6b8 --- /dev/null +++ b/AyCode.Services.Server/SignalRs/SignalRRequestModel.cs @@ -0,0 +1,19 @@ +namespace AyCode.Services.Server.SignalRs; + +public class SignalRRequestModel +{ + public DateTime RequestDateTime; + public DateTime ResponseDateTime; + public object? ResponseByRequestId = null; + + public SignalRRequestModel() + { + RequestDateTime = DateTime.UtcNow; + } + + public SignalRRequestModel(object responseByRequestId) : this() + { + ResponseByRequestId = responseByRequestId; + } + +} \ No newline at end of file diff --git a/AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs b/AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs new file mode 100644 index 0000000..92da9c4 --- /dev/null +++ b/AyCode.Services.Server/SignalRs/TrackingItemHelpers.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using AyCode.Core.Extensions; + +namespace AyCode.Services.Server.SignalRs; + +public static class TrackingItemHelpers +{ + public static T JsonClone(T source) => source.ToJson().JsonTo()!; + + public static T ReflectionClone(T source) + { + var type = source!.GetType(); + + if (type.IsPrimitive || typeof(string) == type) + return source; + + if (type.IsArray) + { + var elementType = Type.GetType(type.FullName!.Replace("[]", string.Empty))!; + var array = (source as Array)!; + var cloned = Array.CreateInstance(elementType, array.Length); + + for (var i = 0; i < array.Length; i++) + cloned.SetValue(ReflectionClone(array.GetValue(i)), i); + + return (T)Convert.ChangeType(cloned, type); + } + + var clone = Activator.CreateInstance(type); + + while (type != null && type != typeof(object)) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var fieldValue = field.GetValue(source); + if (fieldValue == null) continue; + + field.SetValue(clone, ReflectionClone(fieldValue)); + } + + type = type.BaseType; + } + + return (T)clone!; + } +} \ No newline at end of file