using System.Buffers; using System.Security.Claims; using AyCode.Core; using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Loggers; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Serializers.Jsons; using AyCode.Models.Server.DynamicMethods; using AyCode.Services.SignalRs; 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 TLogger Logger = logger; protected IConfiguration Configuration = configuration; protected AcSerializerOptions SerializerOptions = new AcBinarySerializerOptions(); /// /// Enable diagnostic logging for binary serialization debugging. /// Set to true to log hex dumps of serialized response data. /// public static bool EnableBinaryDiagnostics { get; set; } = false; #region Connection Lifecycle public override async Task OnConnectedAsync() { Logger.Debug($"Server OnConnectedAsync; ConnectionId: {GetConnectionId()}; UserIdentifier: {GetUserIdentifier()}"); LogContextUserNameAndId(); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var connectionId = GetConnectionId(); var userIdentifier = GetUserIdentifier(); if (exception == null) Logger.Debug($"Server OnDisconnectedAsync; ConnectionId: {connectionId}; UserIdentifier: {userIdentifier}"); else Logger.Error($"Server OnDisconnectedAsync; ConnectionId: {connectionId}; UserIdentifier: {userIdentifier}", exception); LogContextUserNameAndId(); await base.OnDisconnectedAsync(exception); } #endregion #region Message Processing public virtual Task OnReceiveMessage(int messageTag, byte[]? messageBytes, int? requestId) { return ProcessOnReceiveMessage(messageTag, messageBytes, requestId, null); } protected virtual async Task ProcessOnReceiveMessage(int messageTag, byte[]? message, int? requestId, Func? notFoundCallback) { var tagName = ConstHelper.NameByValue(messageTag); if (message is { Length: 0 }) { Logger.Warning($"message.Length == 0! Server OnReceiveMessage; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}"); } else { Logger.Debug($"[{message?.Length:N0}b] Server OnReceiveMessage; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}"); } try { if (AcDomain.IsDeveloperVersion) LogContextUserNameAndId(); if (TryFindAndInvokeMethod(messageTag, message, tagName, out var responseData)) { var responseMessage = CreateResponseMessage(messageTag, SignalResponseStatus.Success, responseData); if (Logger.LogLevel <= LogLevel.Debug) { var responseSize = GetResponseSize(responseMessage); Logger.Debug($"[{responseSize / 1024}kb] responseData serialized ({SerializerOptions.SerializerType})"); } // Log binary diagnostics if enabled if (EnableBinaryDiagnostics && responseMessage is SignalResponseDataMessage dataMsg && dataMsg.ResponseData != null) { LogResponseDataDiagnostics(messageTag, tagName, requestId, dataMsg.ResponseData); } await ResponseToCaller(messageTag, responseMessage, requestId); return; } Logger.Warning($"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, CreateResponseMessage(messageTag, SignalResponseStatus.Error, null), requestId); } /// /// Reads a VarUInt from byte array at given position. /// private static (uint value, int bytesRead) ReadVarUIntFromBytes(byte[] data, int startPos) { uint value = 0; var shift = 0; var bytesRead = 0; while (startPos + bytesRead < data.Length) { var b = data[startPos + bytesRead]; bytesRead++; value |= (uint)(b & 0x7F) << shift; if ((b & 0x80) == 0) break; shift += 7; if (shift > 35) break; } return (value, bytesRead); } /// /// Logs type information about the response data before serialization. /// private void LogResponseDataTypeInfo(object responseData) { try { var type = responseData.GetType(); Logger.Info($"=== SERVER RESPONSE TYPE INFO (BEFORE SERIALIZE) ==="); Logger.Info($"Runtime Type: {type.Name}"); Logger.Info($"FullName: {type.FullName}"); Logger.Info($"Namespace: {type.Namespace}"); Logger.Info($"Assembly: {type.Assembly.GetName().Name} v{type.Assembly.GetName().Version}"); Logger.Info($"AssemblyQualifiedName: {type.AssemblyQualifiedName}"); Logger.Info($"Assembly Location: {type.Assembly.Location}"); // For collections, log element type info if (type.IsGenericType) { var genericArgs = type.GetGenericArguments(); Logger.Info($"Generic Arguments: [{string.Join(", ", genericArgs.Select(t => t.FullName))}]"); if (genericArgs.Length == 1) { var elementType = genericArgs[0]; Logger.Info($"--- ELEMENT TYPE INFO ---"); Logger.Info($"Element Type: {elementType.Name}"); Logger.Info($"Element FullName: {elementType.FullName}"); Logger.Info($"Element Namespace: {elementType.Namespace}"); Logger.Info($"Element Assembly: {elementType.Assembly.GetName().Name} v{elementType.Assembly.GetName().Version}"); Logger.Info($"Element AssemblyQualifiedName: {elementType.AssemblyQualifiedName}"); Logger.Info($"Element Assembly Location: {elementType.Assembly.Location}"); Logger.Info($"Element BaseType: {elementType.BaseType?.FullName ?? "null"}"); // Log inheritance chain var baseType = elementType.BaseType; var inheritanceChain = new List(); while (baseType != null && baseType != typeof(object)) { inheritanceChain.Add($"{baseType.Name} ({baseType.Assembly.GetName().Name})"); baseType = baseType.BaseType; } if (inheritanceChain.Count > 0) { Logger.Info($"Element Inheritance: {string.Join(" -> ", inheritanceChain)}"); } LogTypePropertiesServer(elementType, "Element"); } } else { Logger.Info($"BaseType: {type.BaseType?.FullName ?? "null"}"); LogTypePropertiesServer(type, "Response"); } } catch (Exception ex) { Logger.Warning($"Failed to log response type info: {ex.Message}"); } } /// /// Logs all properties of a type with their declaring types. /// private void LogTypePropertiesServer(Type type, string prefix) { var props = type.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0) .ToArray(); // Log in declaration order (not alphabetically) Logger.Info($"{prefix} Property Count: {props.Length}"); for (var i = 0; i < props.Length; i++) { var p = props[i]; var declaringType = p.DeclaringType?.Name ?? "?"; var declaringAssembly = p.DeclaringType?.Assembly.GetName().Name ?? "?"; Logger.Info($" {prefix}[{i}]: {p.Name} : {p.PropertyType.Name} (declared in {declaringType} @ {declaringAssembly})"); } } /// /// Logs diagnostic information about the ResponseData binary for debugging serialization issues. /// private void LogResponseDataDiagnostics(int messageTag, string tagName, int? requestId, byte[] responseData) { try { Logger.Info($"=== SERVER RESPONSE DATA DIAGNOSTICS (AFTER SERIALIZE) ==="); Logger.Info($"Tag: {messageTag} ({tagName}); RequestId: {requestId}; ResponseData.Length: {responseData.Length}"); Logger.Info($"HEX (first 500 bytes): {Convert.ToHexString(responseData.AsSpan(0, Math.Min(500, responseData.Length)))}"); if (responseData.Length >= 3) { var version = responseData[0]; var marker = responseData[1]; Logger.Info($"Version: {version}; Marker: 0x{marker:X2}"); if ((marker & 0x10) != 0) { // Read property count as VarUInt var pos = 2; var (propCount, bytesRead) = ReadVarUIntFromBytes(responseData, pos); pos += bytesRead; Logger.Info($"Header property count: {propCount}"); for (var i = 0; i < (int)propCount && pos < responseData.Length; i++) { // Read string length as VarUInt var (strLen, strLenBytes) = ReadVarUIntFromBytes(responseData, pos); pos += strLenBytes; if (pos + (int)strLen <= responseData.Length) { var propName = System.Text.Encoding.UTF8.GetString(responseData, pos, (int)strLen); pos += (int)strLen; Logger.Info($" Header[{i}]: '{propName}'"); } else { Logger.Info($" Header[{i}]: "); break; } } } } } catch (Exception ex) { Logger.Warning($"Failed to log response data diagnostics: {ex.Message}"); } } /// /// Creates a response message using the configured serializer. /// protected virtual ISignalRMessage CreateResponseMessage(int messageTag, SignalResponseStatus status, object? responseData) { return new SignalResponseDataMessage(messageTag, status, responseData, SerializerOptions); } /// /// Gets the size of the response data for logging purposes. /// private static int GetResponseSize(ISignalRMessage responseMessage) { return responseMessage is SignalResponseDataMessage dataMsg ? dataMsg.ResponseData?.Length ?? 0 : 0; } /// /// Finds and invokes the method registered for the given message tag. /// private bool TryFindAndInvokeMethod(int messageTag, byte[]? message, string tagName, out object? responseData) { responseData = null; foreach (var methodsByDeclaringObject in DynamicMethodCallModels) { if (!methodsByDeclaringObject.MethodsByMessageTag.TryGetValue(messageTag, out var methodInfoModel)) continue; var methodName = $"{methodsByDeclaringObject.InstanceObject.GetType().Name}.{methodInfoModel.MethodInfo.Name}"; var paramValues = DeserializeParameters(message, methodInfoModel, tagName, methodName); if (paramValues == null) Logger.Debug($"Found dynamic method for the tag! method: {methodName}(); {tagName}"); else Logger.Debug($"Found dynamic method for the tag! method: {methodName}({string.Join(", ", methodInfoModel.ParamInfos.Select(x => x.Name))}); {tagName}"); responseData = methodInfoModel.MethodInfo.InvokeMethod(methodsByDeclaringObject.InstanceObject, paramValues); // Log type information if diagnostics enabled if (EnableBinaryDiagnostics && responseData != null) { LogResponseDataTypeInfo(responseData); } if (methodInfoModel.Attribute.SendToOtherClientType != SendToClientType.None) SendMessageToOthers(methodInfoModel.Attribute.SendToOtherClientTag, responseData).Forget(); return true; } return false; } /// /// Deserializes parameters from the message based on method signature. /// Uses Binary serialization for message wrapper. /// private static object[]? DeserializeParameters(byte[]? message, AcMethodInfoModel methodInfoModel, string tagName, string methodName) { if (methodInfoModel.ParamInfos is not { Length: > 0 }) return null; if (message is null or { Length: 0 }) throw new ArgumentException($"Message is null or empty but method '{methodName}' requires {methodInfoModel.ParamInfos.Length} parameter(s); {tagName}"); var paramValues = new object[methodInfoModel.ParamInfos.Length]; var firstParamType = methodInfoModel.ParamInfos[0].ParameterType; // First, try to deserialize as SignalPostJsonMessage to get raw PostDataJson var msgBase = SignalRSerializationHelper.DeserializeFromBinary(message); if (msgBase?.PostDataJson == null || string.IsNullOrEmpty(msgBase.PostDataJson)) { throw new ArgumentException($"Failed to deserialize message for method '{methodName}'; {tagName}"); } var json = msgBase.PostDataJson; // Check if it's an IdMessage format (contains "Ids" property) if (json.Contains("\"Ids\"")) { // Parse as IdMessage - each Id is a JSON string for a parameter var idMessage = json.JsonTo(); if (idMessage?.Ids != null && idMessage.Ids.Count > 0) { for (var i = 0; i < idMessage.Ids.Count && i < methodInfoModel.ParamInfos.Length; i++) { var paramType = methodInfoModel.ParamInfos[i].ParameterType; paramValues[i] = AcJsonDeserializer.Deserialize(idMessage.Ids[i], paramType)!; } return paramValues; } } // Single complex object - deserialize directly from PostDataJson paramValues[0] = json.JsonTo(firstParamType)!; return paramValues; } /// /// Determines if a type should use IdMessage format. /// private static bool IsPrimitiveOrStringOrEnum(Type type) { return type == typeof(string) || type.IsEnum || type.IsValueType || type == typeof(DateTime); } #endregion #region Response Methods protected virtual Task ResponseToCallerWithContent(int messageTag, object? content) => ResponseToCaller(messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null); protected virtual Task ResponseToCaller(int messageTag, ISignalRMessage message, int? requestId) => SendMessageToClient(Clients.Caller, messageTag, message, requestId); protected virtual Task SendMessageToUserIdWithContent(string userId, int messageTag, object? content) => SendMessageToUserIdInternal(userId, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null); protected virtual Task SendMessageToUserIdInternal(string userId, int messageTag, ISignalRMessage message, int? requestId) => SendMessageToClient(Clients.User(userId), messageTag, message, requestId); protected virtual Task SendMessageToConnectionIdWithContent(string connectionId, int messageTag, object? content) => SendMessageToConnectionIdInternal(connectionId, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null); protected virtual Task SendMessageToConnectionIdInternal(string connectionId, int messageTag, ISignalRMessage message, int? requestId) => SendMessageToClient(Clients.Client(connectionId), messageTag, message, requestId); protected virtual Task SendMessageToOthers(int messageTag, object? content) => SendMessageToClient(Clients.Others, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null); protected virtual Task SendMessageToAll(int messageTag, object? content) => SendMessageToClient(Clients.All, messageTag, CreateResponseMessage(messageTag, SignalResponseStatus.Success, content), null); /// /// Sends message to client using Binary serialization. /// protected virtual async Task SendMessageToClient(IAcSignalRHubItemServer sendTo, int messageTag, ISignalRMessage message, int? requestId = null) { var responseBytes = SignalRSerializationHelper.SerializeToBinary(message); var tagName = ConstHelper.NameByValue(messageTag); Logger.Debug($"[{responseBytes.Length / 1024}kb] Server sending message to client; requestId: {requestId}; Aborted: {IsConnectionAborted()}; ConnectionId: {GetConnectionId()}; {tagName}"); await sendTo.OnReceiveMessage(messageTag, responseBytes, requestId); Logger.Debug($"Server sent message to client; requestId: {requestId}; ConnectionId: {GetConnectionId()}; {tagName}"); } #endregion #region Context Accessor Methods protected virtual string GetConnectionId() => Context.ConnectionId; protected virtual bool IsConnectionAborted() => Context.ConnectionAborted.IsCancellationRequested; protected virtual string? GetUserIdentifier() => Context.UserIdentifier; protected virtual ClaimsPrincipal? GetUser() => Context.User; #endregion #region Logging protected virtual void LogContextUserNameAndId() { var user = GetUser(); if (user == null) return; var userName = user.Identity?.Name; Guid.TryParse(user.FindFirstValue(ClaimTypes.NameIdentifier), out var userId); if (AcDomain.IsDeveloperVersion) Logger.WarningConditional($"SignalR.Context; userName: {userName}; userId: {userId}"); else Logger.Debug($"SignalR.Context; userName: {userName}; userId: {userId}"); } #endregion }